fix(audit): stop plaintext secrets leaking into the audit log - #3512
Merged
Conversation
… log
Audit rows are readable with app:read (e.g. the auditor role), but the
interceptor diffed request bodies into AuditLog.data with only exact-match,
top-level key redaction — so credentials landed in cleartext where roles denied
the resource's own read permission could see them.
Confirmed leak: POST/PUT /v1/secrets stored the plaintext `value` (key not in
the denylist), readable by an auditor who is intentionally denied secret:read.
Defense in depth, all layers:
- Per-resource body skip (REDACT_BODY_RESOURCES = {secret}): log the action, not
the payload, for credential resources whose secret rides in a generic field.
- Regex key matching: redact clientSecret, secretAccessKey, accessKeyId, etc.
that the exact-match SENSITIVE_KEYS set missed.
- Summarize object elements inside arrays as [Object] (as nested objects already
are), so secrets in generic array fields (browserbase extraFields[].value,
email attachments[].content) can't be logged verbatim.
- @SkipAuditLog the internal mailer (html/attachments carry OTP/magic-links).
Tests: new audit-log.utils.spec (regex, arrays, nested); interceptor asserts a
secret mutation logs the action but never the plaintext value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Addresses cubic review: AdminAuditLogInterceptor.sanitizeBody only skipped exact-match SENSITIVE_KEYS, so credential-shaped fields (clientSecret, secretAccessKey, …) could still leak through the platform-admin audit path. Reuse the shared SENSITIVE_KEY_PATTERN so both interceptors redact consistently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claudfuen
pushed a commit
that referenced
this pull request
Jul 27, 2026
# [3.110.0](v3.109.0...v3.110.0) (2026-07-27) ### Bug Fixes * **app:** make invite modal manual rows scrollable so action buttons stay visible ([#3510](#3510)) ([9926408](9926408)) * **audit:** stop logging read endpoints as mutations + name task-item events ([#3508](#3508)) ([3f9e1cf](3f9e1cf)) * **audit:** stop plaintext secrets leaking into the audit log ([#3512](#3512)) ([55a48fb](55a48fb)) ### Features * **api:** email org owners/admins when a portal access request is submitted (CS-522) ([#3494](#3494)) ([258ce45](258ce45))
Contributor
|
🎉 This PR is included in version 3.110.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary (security)
Audit rows are readable with
app:read(which the auditor role has), but theAuditLogInterceptordiffed request bodies intoAuditLog.datausing only exact-match, top-level key redaction. Credentials therefore landed in cleartext in a store readable by roles that are denied the resource's own read permission.Confirmed, traced leak:
POST/PUT /v1/secretsstored the plaintextvalue(the keyvalueisn't in the denylist) → any auditor (deliberately deniedsecret:read— see the comment atpermissions.ts:50-53) could read org secrets viaGET /v1/audit-logs. Intra-tenant, but auditor is often an external third party, and the plaintext also sits in DB/backups/log pipelines. High severity.I also swept every audited mutation endpoint for the same class of issue and fixed them all.
Fix — defense in depth
REDACT_BODY_RESOURCES = {'secret'}: log the action (Created/Updated/Deleted secret) but never diff the body. This is the only thing that catches a secret in a generically-named field likevalue.sanitizeValuenow redacts keys matching/secret|password|passphrase|credential|token|api[_-]?key|private[_-]?key|totp|access[_-]?key/i, soclientSecret,secretAccessKey,accessKeyId, … are caught (they weren't before).[Object](nested objects already were), closing secrets that ride in generic array fields — e.g. browserbaseextraFields[].value(site login creds) and emailattachments[].content. Primitive array elements (ids, scopes, tags) stay visible.@SkipAuditLogon the internal mailer (/v1/internal/email/send*) —html/attachments carry OTP/magic-links and have little audit value.Sweep result (all other credential endpoints)
Already covered by the above: cloud-security
validate-aws(accessKeyId/secretAccessKey→ regex), integrationconnections/variables/credentials(nested object →[Object]), oauth-appsclientSecret(regex), browserbasetotpSeed. API-key creation returns the key in the response (bodies only are logged) — not affected. Platform admin credential save uses a separate interceptor that never logs bodies.Residual (identifier, not secret): browserbase
username/ oauthclientIdremain visible — login identities, not credentials.Tests
audit-log.utils.spec(new): regex redaction, array-object summarization, nested-object hiding, primitives preserved.audit-log.interceptor.spec: asecretmutation logs the action but the plaintext never appears in the row.Follow-up (not in this PR)
Historical
audit_log.datarows already contain leaked plaintext — a scrub of existing rows + downstream exports should be done separately.🤖 Generated with Claude Code
Summary by cubic
Prevents plaintext secrets from being stored in audit logs. Closes a confirmed leak where users with
app:readcould read secret values viaGET /v1/audit-logs.secretresource; log the action only.clientSecret,secretAccessKey,apiKey,privateKey,totp) in both global and admin audit interceptors.[Object]; keep primitive arrays unchanged.@SkipAuditLogto internal email send endpoints to avoid logginghtmland attachments.Written for commit d54c8dd. Summary will update on new commits.