-
Notifications
You must be signed in to change notification settings - Fork 87
security: add pino log serializer to scrub sensitive data #8233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| /** | ||
| * Pino log serializers that scrub sensitive data (bearer tokens, API keys, | ||
| * authorization headers) from logged values. Applied to every pino instance | ||
| * so secrets never reach log files even when errors bubble up opaque objects. | ||
| * | ||
| * API-key patterns are intentionally duplicated from security/secret-scanner.ts | ||
| * rather than imported — the scanner carries entropy analysis, encoded-secret | ||
| * detection, and other heavyweight logic that a hot-path serializer should not | ||
| * pull in. | ||
| */ | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Sensitive-value patterns (subset of secret-scanner PATTERNS) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const BEARER_RE = /Bearer [A-Za-z0-9._\-]+/g; | ||
|
|
||
| const API_KEY_PATTERNS: RegExp[] = [ | ||
| // AWS | ||
| /AKIA[0-9A-Z]{16}/g, | ||
| // GitHub | ||
| /gh[pousr]_[A-Za-z0-9_]{36,255}/g, | ||
| /github_pat_[A-Za-z0-9_]{22,255}/g, | ||
| // GitLab | ||
| /glpat-[A-Za-z0-9\-_]{20,}/g, | ||
| // Stripe | ||
| /sk_live_[A-Za-z0-9]{24,}/g, | ||
| /rk_live_[A-Za-z0-9]{24,}/g, | ||
| // Slack | ||
| /xoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/g, | ||
| /xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-f0-9]{32}/g, | ||
| // Anthropic | ||
| /sk-ant-[A-Za-z0-9\-_]{80,}/g, | ||
| // OpenAI | ||
| /sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}/g, | ||
| /sk-proj-[A-Za-z0-9\-_]{40,}/g, | ||
| /AIza[A-Za-z0-9\-_]{35}/g, | ||
| /GOCSPX-[A-Za-z0-9\-_]{28}/g, | ||
| // SendGrid | ||
| /SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}/g, | ||
| // Telegram bot token | ||
| /[0-9]{8,10}:[A-Za-z0-9_-]{35}/g, | ||
| // npm | ||
| /npm_[A-Za-z0-9]{36}/g, | ||
| ]; | ||
|
|
||
| // Header names whose values should always be fully redacted | ||
| const SENSITIVE_HEADERS = new Set([ | ||
| 'authorization', | ||
| 'proxy-authorization', | ||
| 'cookie', | ||
| 'set-cookie', | ||
| 'x-api-key', | ||
| 'x-auth-token', | ||
| ]); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // String redaction | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| function redactString(value: string): string { | ||
| let result = value; | ||
|
|
||
| // Redact bearer tokens | ||
| result = result.replace(BEARER_RE, 'Bearer [REDACTED]'); | ||
|
|
||
| // Redact API key patterns | ||
| for (const pattern of API_KEY_PATTERNS) { | ||
| pattern.lastIndex = 0; | ||
| result = result.replace(pattern, '[REDACTED]'); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Deep value redaction — walks objects/arrays and scrubs strings in place | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| function redactValue(value: unknown, depth: number): unknown { | ||
| if (depth > 8) return value; | ||
|
|
||
| if (typeof value === 'string') { | ||
| return redactString(value); | ||
| } | ||
|
|
||
| if (Array.isArray(value)) { | ||
| return value.map((item) => redactValue(item, depth + 1)); | ||
| } | ||
|
|
||
| if (value !== null && typeof value === 'object') { | ||
| const result: Record<string, unknown> = {}; | ||
| for (const [key, val] of Object.entries(value as Record<string, unknown>)) { | ||
| const lowerKey = key.toLowerCase(); | ||
| // Fully redact sensitive header values | ||
| if (SENSITIVE_HEADERS.has(lowerKey)) { | ||
| result[key] = '[REDACTED]'; | ||
| } else { | ||
| result[key] = redactValue(val, depth + 1); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Pino serializers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Pino serializer for the `err` binding — redacts secrets from error messages, | ||
| * stacks, and any attached properties. | ||
| */ | ||
| function errSerializer(err: unknown): unknown { | ||
| return redactValue(err, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Pino serializer for `req` (HTTP request objects) — redacts authorization | ||
| * headers and sensitive values in the URL/body. | ||
| */ | ||
| function reqSerializer(req: unknown): unknown { | ||
| return redactValue(req, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Pino serializer for `res` (HTTP response objects) — redacts sensitive | ||
| * header values that may appear in response logs. | ||
| */ | ||
| function resSerializer(res: unknown): unknown { | ||
| return redactValue(res, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Pino serializers config object. Spread this into the pino options `serializers` | ||
| * field on every logger instance. | ||
| */ | ||
| export const logSerializers: Record<string, (value: unknown) => unknown> = { | ||
| err: errSerializer, | ||
| req: reqSerializer, | ||
| res: resSerializer, | ||
| }; | ||
|
|
||
| // Exported for testing | ||
| export { redactString as _redactString, redactValue as _redactValue }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /** | ||
| * Pino log serializers that scrub sensitive data (bearer tokens, API keys, | ||
| * authorization headers) from logged values. | ||
| * | ||
| * This is a standalone copy for the gateway package — kept in sync with | ||
| * assistant/src/util/log-redact.ts. The gateway has no dependency on the | ||
| * assistant package, so we duplicate the lightweight serializer rather than | ||
| * adding a cross-package import. | ||
| */ | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Sensitive-value patterns | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const BEARER_RE = /Bearer [A-Za-z0-9._\-]+/g; | ||
|
|
||
| const API_KEY_PATTERNS: RegExp[] = [ | ||
| /AKIA[0-9A-Z]{16}/g, | ||
| /gh[pousr]_[A-Za-z0-9_]{36,255}/g, | ||
| /github_pat_[A-Za-z0-9_]{22,255}/g, | ||
| /glpat-[A-Za-z0-9\-_]{20,}/g, | ||
| /sk_live_[A-Za-z0-9]{24,}/g, | ||
| /rk_live_[A-Za-z0-9]{24,}/g, | ||
| /xoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/g, | ||
| /xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-f0-9]{32}/g, | ||
| /sk-ant-[A-Za-z0-9\-_]{80,}/g, | ||
| /sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}/g, | ||
| /sk-proj-[A-Za-z0-9\-_]{40,}/g, | ||
| /AIza[A-Za-z0-9\-_]{35}/g, | ||
| /GOCSPX-[A-Za-z0-9\-_]{28}/g, | ||
| /SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}/g, | ||
| /[0-9]{8,10}:[A-Za-z0-9_-]{35}/g, | ||
| /npm_[A-Za-z0-9]{36}/g, | ||
| ]; | ||
|
|
||
| const SENSITIVE_HEADERS = new Set([ | ||
| "authorization", | ||
| "proxy-authorization", | ||
| "cookie", | ||
| "set-cookie", | ||
| "x-api-key", | ||
| "x-auth-token", | ||
| ]); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // String redaction | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| function redactString(value: string): string { | ||
| let result = value; | ||
| result = result.replace(BEARER_RE, "Bearer [REDACTED]"); | ||
| for (const pattern of API_KEY_PATTERNS) { | ||
| pattern.lastIndex = 0; | ||
| result = result.replace(pattern, "[REDACTED]"); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Deep value redaction | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| function redactValue(value: unknown, depth: number): unknown { | ||
| if (depth > 8) return value; | ||
|
|
||
| if (typeof value === "string") { | ||
| return redactString(value); | ||
| } | ||
|
|
||
| if (Array.isArray(value)) { | ||
| return value.map((item) => redactValue(item, depth + 1)); | ||
| } | ||
|
|
||
| if (value !== null && typeof value === "object") { | ||
| const result: Record<string, unknown> = {}; | ||
| for (const [key, val] of Object.entries(value as Record<string, unknown>)) { | ||
| if (SENSITIVE_HEADERS.has(key.toLowerCase())) { | ||
| result[key] = "[REDACTED]"; | ||
| } else { | ||
| result[key] = redactValue(val, depth + 1); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Pino serializers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export const logSerializers: Record<string, (value: unknown) => unknown> = { | ||
| err: (err) => redactValue(err, 0), | ||
| req: (req) => redactValue(req, 0), | ||
| res: (res) => redactValue(res, 0), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.