Skip to content

fix(audit): stop plaintext secrets leaking into the audit log - #3512

Merged
Marfuen merged 2 commits into
mainfrom
mariano/fix-audit-secret-leak
Jul 27, 2026
Merged

fix(audit): stop plaintext secrets leaking into the audit log#3512
Marfuen merged 2 commits into
mainfrom
mariano/fix-audit-secret-leak

Conversation

@Marfuen

@Marfuen Marfuen commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary (security)

Audit rows are readable with app:read (which the auditor role has), but the AuditLogInterceptor diffed request bodies into AuditLog.data using 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/secrets stored the plaintext value (the key value isn't in the denylist) → any auditor (deliberately denied secret:read — see the comment at permissions.ts:50-53) could read org secrets via GET /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

  1. Per-resource body skipREDACT_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 like value.
  2. Regex key matchingsanitizeValue now redacts keys matching /secret|password|passphrase|credential|token|api[_-]?key|private[_-]?key|totp|access[_-]?key/i, so clientSecret, secretAccessKey, accessKeyId, … are caught (they weren't before).
  3. Array summarization — object elements inside arrays are now summarized as [Object] (nested objects already were), closing secrets that ride in generic array fields — e.g. browserbase extraFields[].value (site login creds) and email attachments[].content. Primitive array elements (ids, scopes, tags) stay visible.
  4. @SkipAuditLog on 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), integration connections/variables/credentials (nested object → [Object]), oauth-apps clientSecret (regex), browserbase totpSeed. 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 / oauth clientId remain 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: a secret mutation logs the action but the plaintext never appears in the row.
  • Audit suite 68/68, typecheck clean.

Follow-up (not in this PR)

Historical audit_log.data rows 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:read could read secret values via GET /v1/audit-logs.

  • Bug Fixes
    • Skip body diffing for the secret resource; log the action only.
    • Redact credential-like keys via regex (e.g., clientSecret, secretAccessKey, apiKey, privateKey, totp) in both global and admin audit interceptors.
    • Summarize object elements inside arrays as [Object]; keep primitive arrays unchanged.
    • Apply @SkipAuditLog to internal email send endpoints to avoid logging html and attachments.
    • Add tests for regex redaction (including admin path), array summarization, nested object hiding, and interceptor behavior.

Written for commit d54c8dd. Summary will update on new commits.

Review in cubic

… 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>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Jul 27, 2026 6:53pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Jul 27, 2026 6:53pm
portal Skipped Skipped Jul 27, 2026 6:53pm

Request Review

@linear

linear Bot commented Jul 27, 2026

Copy link
Copy Markdown

ENG-261

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/audit/audit-log.constants.ts
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>
@vercel
vercel Bot temporarily deployed to Preview – app July 27, 2026 18:52 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal July 27, 2026 18:52 Inactive
@Marfuen
Marfuen merged commit 55a48fb into main Jul 27, 2026
11 checks passed
@Marfuen
Marfuen deleted the mariano/fix-audit-secret-leak branch July 27, 2026 19:11
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))
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.110.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants