Skip to content

Redact secrets from CLI logs, not just CI logs - #2412

Open
ninadbstack wants to merge 2 commits into
masterfrom
PER-9354-redact-clilogs
Open

Redact secrets from CLI logs, not just CI logs#2412
ninadbstack wants to merge 2 commits into
masterfrom
PER-9354-redact-clilogs

Conversation

@ninadbstack

@ninadbstack ninadbstack commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

sendBuildLogs applied redactSecrets to cilogs but sent clilogs through untouched:

const logsObject = {
  clilogs: logger.query(log => !['ci'].includes(log.debug))   // ← unredacted
};
...
logsObject.cilogs = redactSecrets(logger.query(...));          // ← redacted

CLI log entries are the ones that can carry foreign response text — SDK error messages interpolate fields from remote HTTP responses — and /logs content is retrievable via GET /api/v1/logs by anyone who passes authorize(build, :read?), i.e. any project member. There is no server-side redaction on that path. So the half most likely to hold upstream data was the half we weren't redacting.

This routes clilogs through the same existing redactSecrets helper. No new patterns, no new mechanism — secretPatterns.yml already ships ~1750 rules including AWS Access Key ID / cred-file shapes.

Why the memoization is in the same PR

redactSecrets recurses per log entry, and each call did readFileSync + YAML.parse of the ~1750-rule pattern file and recompiled every regex. That was already wasteful for cilogs; sending clilogs (the larger set) through it would have made it a real cost on big builds. Patterns are now parsed and compiled once.

Reusing the /g regexes across replace() calls is safe — String.prototype.replace resets lastIndex on a global pattern, verified:

0 "x [REDACTED] y" lastIndex= 0
1 "x [REDACTED] y" lastIndex= 0
2 "x [REDACTED] y" lastIndex= 0

Behavior notes

  • No change for legitimate users or for support: the full message is preserved, only credential-shaped substrings become [REDACTED].
  • The in-place mutation of logger entries is unchanged from the existing cilogs path and matches the contract documented at packages/logger/src/logger.js:137-146.

Testing

Added redacts secrets from CLI logs, not just CI logs to packages/core/test/percy.test.js — decodes the actual posted /logs payload and asserts the key shape is absent from clilogs and [REDACTED] is present.

Draft because I could not run the suitenode_modules isn't installed in my environment and installing deps is out of scope there. Files pass node --check; the regex-reuse claim above is verified standalone. Please let CI run before marking ready.

Context

Found while assessing PER-9354. That chain finding itself does not reproduce (details in the ticket) — this is an independent gap surfaced along the way, and it stands on its own merits.

🤖 Generated with Claude Code

sendBuildLogs applied redactSecrets to cilogs but sent clilogs through
untouched. CLI log entries can carry upstream response text — SDK errors
interpolate remote response fields into their messages — and /logs content
is retrievable via GET /api/v1/logs by anyone with build read access, so
the half that most often holds foreign response data was the unredacted
half.

Also memoize the pattern file. redactSecrets recurses per log entry and
re-read + re-parsed the ~1750-rule YAML and recompiled every regex on each
call; that was tolerable for cilogs alone but not once clilogs (the larger
set) goes through it. Patterns are now parsed and compiled once. Reusing
the global regexes across replace() calls is safe — replace() resets
lastIndex on a global pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ninadbstack
ninadbstack marked this pull request as ready for review September 1, 2026 11:23
@ninadbstack
ninadbstack requested a review from a team as a code owner September 1, 2026 11:23
Master landed the same fix in #2279 (security: redact CLI logs, bound
regex matching). Both conflicting hunks resolved in favour of master's
version, which is a superset of this branch's: same redactSecrets call
on clilogs in sendBuildLogs, same compile-patterns-once memoization,
plus in-place entry mutation, the ReDoS bound and a semgrep annotation
this branch didn't have.

What remains of this branch is the integration-level sendBuildLogs
redaction test; master's coverage for #2279 is unit-level in
test/unit/utils.test.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
percy.build = { id: 1 };
// A CLI-side log entry can carry upstream response text, so it needs the
// same redaction cilogs already gets.
percy.log.info('leaked from upstream: ASIAY34FZKBOKMUTVV7A');
// A CLI-side log entry can carry upstream response text, so it needs the
// same redaction cilogs already gets.
percy.log.info('leaked from upstream: ASIAY34FZKBOKMUTVV7A');
percy.log.info('ci side: ASIAY34FZKBOKMUTVV7A', {}, true);

const clilogs = JSON.stringify(sent.clilogs);
const cilogs = JSON.stringify(sent.cilogs);
expect(clilogs).not.toContain('ASIAY34FZKBOKMUTVV7A');
const cilogs = JSON.stringify(sent.cilogs);
expect(clilogs).not.toContain('ASIAY34FZKBOKMUTVV7A');
expect(clilogs).toContain('[REDACTED]');
expect(cilogs).not.toContain('ASIAY34FZKBOKMUTVV7A');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants