Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/middleware/redact-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ const SENSITIVE_PARAM_NAMES = new Set([
'refresh_token',
'id_token',
'client_secret',
// OAuth authorization code (RFC 6749 §4.1) and the RFC 7521
// generic-assertion / RFC 7523 JWT-bearer client-assertion
// params. The auth code rides on the redirect URI's query
// string in the standard browser flow; on error redirects it
// occasionally lands back on an arbitrary path with the value
// still attached. The code is single-use and short-lived, but
// a leaked log line with one in it is enough to attempt a
// replay before the legitimate exchange completes. The
// assertion params carry signed JWT/SAML credentials that
// don't expire for minutes to hours and are unambiguously
// sensitive. We don't issue OAuth tokens ourselves, but if an
// operator fronts this API with an OAuth proxy whose error
// handlers bounce through /v1/* we shouldn't be the source of
// the leak.
'code',
'assertion',
'client_assertion',
'password',
'secret',
]);
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/redact-url.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ describe('redactUrl', () => {
expect(redactUrl(42)).toBe(42);
});

test('redacts OAuth authorization code on a redirect-bounce URL', () => {
// OAuth `code` parameter is single-use but a leaked log line
// containing one can be replayed before the legitimate
// exchange completes. RFC 6749 §4.1 puts it on the redirect
// URI's query string verbatim.
const out = redactUrl('/v1/whoami?code=AUTH_CODE_VALUE&state=xyz');
expect(out).toContain('code=<REDACTED>');
expect(out).not.toContain('AUTH_CODE_VALUE');
// state is a CSRF nonce, not sensitive itself — leave it alone
expect(out).toContain('state=xyz');
});

test('redacts the RFC 7521 / 7523 client-assertion JWT', () => {
// RFC 7523 puts a signed JWT in `client_assertion` and
// `assertion`; both are credential-bearing and don't expire
// for minutes to hours.
expect(redactUrl('/x?client_assertion=eyJ.signed.jwt&grant_type=client_credentials'))
.toContain('client_assertion=<REDACTED>');
expect(redactUrl('/x?assertion=eyJ.SAML.creds'))
.toBe('/x?assertion=<REDACTED>');
});

test('does not throw on malformed percent-encoding in param name', () => {
// decodeURIComponent throws URIError on `%FF` (incomplete UTF-8
// sequence) or `%ZZ` (invalid hex). pino-http would invoke this
Expand Down