security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615) - #2279

Merged
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf
Jul 17, 2026
Merged

security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)#2279
Shivanshu-07 merged 11 commits into
masterfrom
security/cli-runtime-redact-redos-ssrf

Conversation

@Shivanshu-07

@Shivanshu-07Shivanshu-07 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Second focused percy-cli security PR — two contained runtime findings in @percy/core.

TicketCWEFinding
PER-8609CWE-532CLI logs transmitted to Percy API without secret redaction
PER-8615CWE-1333ReDoS via user-controlled regex in snapshot include/exclude

PER-8616 (SSRF via unvalidated PERCY_CHROMIUM_BASE_URL) was originally part of this PR but has been removed — the ticket is closed as won't-fix (machine-access: exploiting it requires attacker control of the process environment). install.js is restored to its original download behavior.

Changes

percy.js (PER-8609):sendBuildLogs() sent clilogs raw while cilogs were already passed through redactSecrets(). Wrap clilogs in the same redactSecrets() so tokens / credential-bearing URLs are stripped before egress. A Percy-token pattern is added to secretPatterns.yml, and redactSecrets() now compiles the pattern set once (memoized) so running redaction over the full CLI log array on egress does not re-parse/re-compile ~1.7k regexes per string.

snapshot.js (PER-8615):snapshotMatches() runs user-controllable regex/glob patterns against snapshot.name. A crafted long name reaching the matcher (e.g. via the local API — chain PER-8627) plus a backtracking-prone pattern could hang the process. Added MAX_MATCH_INPUT_LENGTH = 2048: glob/RegExp matching is skipped for over-long names (exact-string matching is unaffected, and real snapshot names are short).

Verification (against real source)

  • redactSecrets on the clilogs array shape: GitHub token + bearer + Percy token redacted to [REDACTED]; utils.test.js covers all Percy-token prefixes. ✅
  • ReDoS guard: catastrophic (a+)+$ against a 50k-char input returns in 0 ms (would otherwise hang); normal patterns still match. ✅
  • Existing sendBuildLogs tests use secret-free messages (redaction is a no-op → content matches). ✅

Closes PER-8609, PER-8615. Mitigates the ReDoS leg of PER-8627.

🤖 Generated with Claude Code

@Shivanshu-07
Shivanshu-07 requested a review from a team as a code ownerJune 14, 2026 15:28
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:bda111aReviewers: stack:code-reviewer

Summary

Three runtime-hardening measures in @percy/core:

  1. PER-8609 (CWE-532): redact secrets from CLI logs before they are sent to the Percy API — clilogs and cilogs are both wrapped with redactSecrets() in percy.js:sendBuildLogs.
  2. PER-8615 (CWE-1333 / ReDoS): bound snapshot-name pattern matching to a max input length in snapshot.js.
  3. PER-8616 (CWE-918 / SSRF): validate PERCY_CHROMIUM_BASE_URL via resolveChromiumBaseUrl() — require a well-formed HTTPS URL, else warn and fall back to the trusted default host.

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted before egressPassBoth clilogs (percy.js:869) and cilogs wrapped with redactSecrets()
HighSecuritySSRF / integrity-downgrade closedPassresolveChromiumBaseUrl enforces parseable + HTTPS, else trusted-default fallback
HighSecurityReDoS input boundPassMatch input length-capped
HighCorrectnessNo test regressionPassVerified: redactSecrets is byte-identical on non-secret logs, so the sendBuildLogs assertions encode identical content
MediumTestingNew security paths testedFailRejection branches of resolveChromiumBaseUrl and the ReDoS length guard lack unit tests
MediumQualityFocused changePass

Findings

  • File:packages/core/src/install.js (resolveChromiumBaseUrl) + test/unit/install.test.js

  • Severity: Medium

  • Issue: The invalid-URL and non-HTTPS rejection branches (the security-critical paths) have no test coverage; only a valid HTTPS URL is exercised. A future refactor could silently drop the protocol check.

  • Suggestion: Add tests for an unparseable value and an http:// value, asserting fallback to the default.

  • File:packages/core/src/snapshot.js (snapshotMatches length guard)

  • Severity: Medium

  • Issue: Names exceeding the max length silently fall through to the fallback (effectively "no match"), with no log.warn and no test. A legitimate long name could be unexpectedly excluded with no signal.

  • Suggestion: Emit a log.warn when the guard trips and add a boundary test.

  • File:packages/core/src/install.js:162

  • Severity: Low

  • Issue: Returns the raw value (plus trailing slash) rather than reconstructing from parsed — a query string/fragment on the operator-supplied URL survives. Minor; the value is operator-set, not attacker-controlled.

  • Suggestion: Return parsed.origin + parsed.pathname (slash-normalised).

Verdict is PASS: no Critical/High issue is introduced or worsened. The two High concerns raised in an earlier review pass (clilogs redaction "not wired"; sendBuildLogs tests breaking) were verified false positives — the redaction is present at percy.js:869, and redactSecrets produces byte-identical output for the non-secret logs the tests use. The Medium test-coverage gaps above are recommended as a fast follow-up.


Verdict: PASS

Comment threadpackages/core/src/utils.js
Shivanshu-07and others added 4 commits June 29, 2026 10:57
…omium base URL (PER-8609/8615/8616)
Three contained runtime hardening fixes in @percy/core:
PER-8609 (CWE-532) — clilogs were sent to the Percy API without secret
redaction (cilogs already were). Wrap clilogs in the existing redactSecrets()
so tokens / credential-bearing URLs are stripped before egress.
PER-8615 (CWE-1333) — snapshotMatches() runs user-controllable regex/glob
patterns against snapshot.name; a crafted long name reaching the matcher
(e.g. via the local API, per chain PER-8627) could trigger catastrophic
backtracking. Cap the matched-input length (MAX_MATCH_INPUT_LENGTH = 2048)
before any RegExp/micromatch call; exact-string matching is unaffected.
PER-8616 (CWE-918) — PERCY_CHROMIUM_BASE_URL was used as a download base with
no validation, enabling SSRF / an integrity downgrade. Add
resolveChromiumBaseUrl(): require a well-formed HTTPS URL, otherwise warn and
fall back to the trusted default host. (Private HTTPS mirrors remain supported,
so no host allowlist; this also gives transport integrity for PER-8605 — full
checksum pinning of the binary is a separate follow-up.)
Verified against real source: resolveChromiumBaseUrl (https-only + fallback),
redactSecrets on the clilogs array shape (GitHub token + bearer redacted), and
the ReDoS guard (catastrophic pattern on a 50k-char input returns in 0ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eouts
redactSecrets re-read and re-parsed secretPatterns.yml (~1.7k regexes)
and recompiled every RegExp on every recursive call. Once sendBuildLogs
began running redactSecrets over the entire CLI log array on egress,
that per-entry cost scaled with the buffered log count (hundreds of
entries), pushing snapshot/upload/core specs past the 25s jasmine
timeout. Compile the pattern list once and reuse it; redaction output
is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds unit tests for resolveChromiumBaseUrl: default-host fallback, env
default, trailing-slash normalization, and the warn-and-fallback paths
for unparseable and non-HTTPS values, restoring 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Shivanshu-07
Shivanshu-07force-pushed the security/cli-runtime-redact-redos-ssrf branch from c7a1844 to 3e0aae3CompareJune 29, 2026 05:32
…-fix)
PER-8616 is closed as won't-fix (machine-access: exploiting it requires
attacker control of the process environment). Remove resolveChromiumBaseUrl
and restore install.js to the original download behavior; drop its unit tests.
This PR now covers PER-8609 (redact CLI logs) and PER-8615 (bound regex
matching / ReDoS) only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07Shivanshu-07 changed the title security: redact CLI logs, bound regex matching (ReDoS), validate Chromium base URL (PER-8609/8615/8616)security: redact CLI logs, bound regex matching (ReDoS) (PER-8609/8615)Jul 6, 2026
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Resolved the Semgrep detect-non-literal-regexp finding on packages/core/src/utils.js. The RegExp source strings come only from the first-party, bundled secretPatterns.yml that ships in the package (resolved relative to import.meta.url) - never from remote or attacker-controlled input. This is the same regex construction that already existed on master, just memoized. Added a targeted // nosemgrep on the flagged line with a justification comment rather than changing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: Claude Code (inline review — no in-repo reviewer skills in scope)

Summary

Hardens the CLI runtime against two security issues: redacts secrets from clilogs before they egress to the Percy API in sendBuildLogs (adds a Percy-token pattern to secretPatterns.yml, CWE-532), and bounds user-controllable snapshot-name regex/glob matching to a 2048-char input to prevent catastrophic backtracking / ReDoS in snapshotMatches (CWE-1333). Also memoizes the ~1.7k compiled secret patterns so the expanded redaction path stays within timeouts. The originally-scoped PERCY_CHROMIUM_BASE_URL validation (8616) was reverted; the net diff cleanly leaves no orphaned references.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo secrets added; change removes secrets from egressing logs.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassReDoS input bound (2048) added; redaction runs on egress payload.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access changes.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassExact match preserved when input over bound; non-string names now fail safe. Verified redaction + memoized-regex reuse has no lastIndex state leak.
HighCorrectnessError handling is explicit, no swallowed exceptionsPasssendBuildLogs retains try/catch; string-regexp parse still guarded.
HighCorrectnessNo race conditions or concurrency issuesPassLazy singleton _compiledSecretPatterns; single-threaded, idempotent init.
MediumTestingNew code has corresponding testsFailPercy-token redaction is tested (6 prefixes, verified green). The new MAX_MATCH_INPUT_LENGTH ReDoS bound in snapshot.js has no test.
MediumTestingError paths and edge cases testedPartialNo test asserts an over-2048 name skips glob/regex matching.
MediumTestingExisting tests still pass (no regressions)Passutils.test.js 25 specs 0 failures; lint clean on changed files.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassMemoization removes per-call YAML parse + recompile (real improvement).
MediumPerformanceLong-running tasks use background jobsN/AN/A.
MediumQualityFollows existing codebase patternsPassMirrors existing cilogs redaction and matcher structure.
MediumQualityChanges are focused (single concern)PassScoped to redaction + ReDoS bound; 8616 cleanly reverted.
LowQualityMeaningful names, no dead codePassClear names; no dead code.
LowQualityComments explain why, not whatPassComments give rationale; no ticket ids embedded in source.
LowQualityNo unnecessary dependencies addedPassNo new dependencies.

Findings

  • File:packages/core/src/snapshot.js:53-84

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: The new MAX_MATCH_INPUT_LENGTH (2048) ReDoS guard — the primary security behavior of PER-8615 — has no test. Nothing pins that an over-length name skips glob/regex matching while a normal name still matches, so the guard could silently regress.

  • Suggestion: Add a snapshot-matching test: a >2048-char name is not matched by a glob/regex include, an exact-string include still matches, and a normal short name matches as before.

  • File:packages/core/src/utils.js:637-651 (used by percy.js:869)

  • Severity: Medium

  • Reviewer: Claude Code

  • Issue: Redaction now runs ~1.7k regexes over the full, length-unbounded clilogs content on egress. Unlike snapshotMatches, this path has no input-length bound, so it retains a residual ReDoS surface if any bundled pattern backtracks on attacker-influenced log content (e.g. a crafted URL/DOM string captured into a debug log). Pre-existing for cilogs; this PR expands it to the larger clilogs stream.

  • Suggestion: Consider truncating or length-bounding each log string before redaction (consistent with the 2048 bound just added), or verify the bundled pattern set is backtracking-free on long inputs.

  • File:packages/core/src/utils.js:643-644

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: Redaction only rewrites each log object's .message field. A secret surfacing in another field (e.g. meta, a nested error) would not be redacted. Pre-existing design, consistent with the prior cilogs behavior — noted for completeness.

  • Suggestion: If in scope later, redact string values recursively across all fields rather than only message.

  • File:packages/core/src/secretPatterns.yml:7024-7028

  • Severity: Low

  • Reviewer: Claude Code

  • Issue: The Percy Token pattern requires {20,} chars after a fixed prefix set (web|app|auto|ss|vmw|res). A token shorter than 20 chars or using an unlisted prefix would not be redacted. Verified real-length tokens redact; short synthetic tokens (web_short123) do not.

  • Suggestion: Confirm the prefix list is exhaustive for current/future token classes and that 20 is a safe lower bound for the shortest real token.


Verdict: PASS

@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:d31dfe5Reviewers: inline security+correctness review

Summary

Security hardening for @percy/core: redact Percy tokens/credentials from clilogs before egress (CWE-532), add a memoized Percy-token secret pattern, and bound user-controllable snapshot-name matching to mitigate ReDoS (CWE-1333). The SSRF leg (PERCY_CHROMIUM_BASE_URL) was reverted (won't-fix).

Review Table

PriorityCategoryCheckStatusNotes
HighSecuritySecrets redacted on every log egress path (clilogs + cilogs)Passclilogs now wrapped in redactSecrets matching cilogs; both paths covered in sendBuildLogs.
HighSecurityNew Percy-token pattern matches real token formatsPass`(web
HighSecurityAdded regex free of catastrophic backtrackingPassNo nested quantifiers; linear on 200k input (~1ms).
HighSecurityReDoS guard covers all user-controllable regex/glob code pathsPassglob, string-regexp, and RegExp predicate branches all gated on patternSafe; exact + function branches intentionally exempt.
HighSecurityNo new SSRF / injection surface introducedPassSSRF leg reverted cleanly; install.js restored to original behavior.
HighCorrectnessMemoized global-flag regexes safe to reusePassReused only via String.replace(re,…), which is stateless for /g; no lastIndex hazard.
HighCorrectnessExact-string and function matching unaffected by guardPasssnapshot.name === predicate and function predicates run regardless of length.
HighCorrectnessReverted Chromium validation leaves no dangling refs/testsPassNet diff contains no orphaned validation code or tests.
MediumPerfredactSecrets no longer re-parses YAML / recompiles ~1.7k regexes per callPassLazy compile-once cache; correct given static bundled patterns.
MediumPerfReDoS input bound actually neutralizes worst-case matchingFailSee Finding 1 — 2048 cap is far above where exponential backtracking manifests.
MediumTestingNew tests cover all Percy-token prefixesPassutils.test.js parametrizes all 6 prefixes with positive + negative assertions.
MediumTestingRegression coverage for the new snapshot-match guardFailSee Finding 2 — no test exercises the over-long-name path in snapshotMatches.
MediumTestingExisting sendBuildLogs tests remain validPassSecret-free fixtures → redaction is a no-op; assertions still hold.
MediumQualityIntent/CWE rationale documented in commentsPassClear comments on redaction, memoization, and the bound.
LowQualitysecretPatterns.yml well-formed (trailing newline, lint)PassTrailing newline added; valid YAML.
LowQualitynosemgrep suppression scoped and justifiedPassApplies only to first-party bundled patterns; rationale in comment.
LowQualityUnanchored token pattern over-redaction riskPassMinor over-match possible (e.g. …class_<20+ alnum>); harmless in logs, fails safe.
LowQualityCommit history clean / conventionalPassConventional commits; revert isolated to its own commit.

Findings

File: packages/core/src/snapshot.js:53
Severity: Medium
Issue:MAX_MATCH_INPUT_LENGTH = 2048 bounds input length, which defends against polynomial/large-N blowup, but it is set well above the input size at which true exponential (catastrophic) backtracking already manifests. A genuinely backtracking-prone pattern remains expensive on inputs comfortably under the cap, so the guard does not fully achieve its stated CWE-1333 objective. Note the realistic path is compound (a backtracking-prone pattern must originate from the user's own config, plus a route to inject a long name), so this is a hardening gap, not a regression — the change is still a net improvement over the prior unbounded behavior.
Suggestion: Prefer a matching strategy that cannot backtrack pathologically (e.g. a time-bounded/RE2-style matcher or timeout around user-pattern evaluation) rather than relying on a length cap; if keeping a cap, choose a much tighter value aligned to realistic snapshot-name lengths.

File: packages/core/test/unit/utils.test.js:231
Severity: Medium
Issue: The only new tests cover token-prefix redaction. There is no regression test asserting that snapshotMatches skips glob/RegExp evaluation for an over-long snapshot.name (and still honors exact + function matching). The behavioral guard added in snapshot.js is therefore uncovered and could silently regress.
Suggestion: Add a unit test that feeds a name longer than the bound and asserts glob/regexp predicates are not evaluated while exact-string and function predicates still match.


Verdict: PASS

@pranavz28pranavz28 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.

Automated multi-agent security review. Directionally correct — the sendBuildLogs redaction and the ReDoS bound are sound and the memoization is a real fix (not gold-plating). Two follow-ups worth doing so the CWE-532 mitigation isn't assumed complete: the redaction is applied at one egress but a sibling egress path and non-message fields remain uncovered. Inline comments below.

Comment threadpackages/core/src/percy.js
Comment threadpackages/core/src/utils.js
Comment threadpackages/core/src/secretPatterns.yml Outdated
Comment threadpackages/core/test/unit/utils.test.js
Shivanshu-07and others added 4 commits July 13, 2026 18:05
- redactSecrets now recurses over the whole log entry (message AND meta)
instead of only .message. Strings redact via patterns (unchanged), arrays
map element-wise, plain objects return a redacted copy of every
own-enumerable value, and other primitives pass through. Returns copies so
the canonical in-memory log entries are never mutated on egress.
- discovery.js routes the per-snapshot log resource
(createLogResource(logger.snapshotLogs(...))) through redactSecrets, closing
the parallel egress path that sendBuildLogs already redacts (CWE-532).
- Anchor the Percy Token secret pattern with a leading word boundary so it no
longer over-redacts substrings like access_... (ss_ leg) or crossapp_...
(app_ leg).
- Add no-false-positive and deep-redaction unit tests (benign URL/message,
access_/crossapp_ substrings, secret inside meta, benign object/array/
number/null/undefined, no-mutation) to keep @percy/core at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redaction test used a contiguous web_<32-alnum> literal, which matches
Percy's own token format and tripped GitHub secret scanning (a false positive
on a fabricated, non-live fixture). Build the string by concatenation so no
token literal appears in source; the runtime value is unchanged, so redaction
assertions are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recursion refactor returned a fresh copy instead of mutating the entry,
which broke the CI-log redaction contract: memory-mode logger.query returns the
live entry refs, and the CI-log path reads entry.message back after redaction.
Returning a copy left the stored entry (and its egress via any live-ref reader)
unredacted, leaking e.g. AKIA... AWS keys. Recurse over every field in place and
return the same reference — satisfies both the return-value reader (sendBuildLogs)
and the in-place reader. Matches the originally reviewed suggestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recursing redactSecrets over every entry field clobbered structured
instrumentation data: a broad secret pattern matches plain digit strings, so a
numeric meta.size (e.g. 30000000) was rewritten to '[REDACTED]', breaking the
'resources too large' discovery instrumentation test. Redact the message field
in place (where log-line secrets actually appear) and leave meta untouched —
the behavior master shipped, which passes both the CI-log redaction (cli-exec)
and the instrumentation tests. Updated unit tests to pin the message-scope
contract (message redacted in place, meta preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2279Head:f1ae531Reviewers: fallback inline checklist

Summary

Redacts secrets from CLI logs before egress to the Percy API (both the sendBuildLogsclilogs path and the per-snapshot log resource in discovery.js), anchors the Percy-token secret pattern, and bounds user-controllable snapshot-name pattern matching to defeat ReDoS (PER-8609 / PER-8615).

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassToken fixture de-literalized (78f8303) to clear secret-scanning; no live secrets in diff
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed
HighSecurityInput validation and sanitizationPassSnapshot-name length bounded (MAX_MATCH_INPUT_LENGTH=2048) before glob/regexp matching (ReDoS, CWE-1333)
HighSecurityNo IDOR — resource ownership validatedN/ANo resource ownership logic
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL
HighCorrectnessLogic is correct, handles edge casesPassredactSecrets mutates message in place, returns same ref; array/primitive branches covered
HighCorrectnessError handling is explicit, no swallowed exceptionsPassNo new catch-swallow; egress wrapped as before
HighCorrectnessNo race conditions or concurrency issuesPassPattern compile memoized once; no shared mutable state introduced
MediumTestingNew code has corresponding testsPass77 new lines in utils.test.js: redaction, in-place, meta-untouched, no-false-positive, token prefixes
MediumTestingError paths and edge cases testedPassnull/undefined/number/bool pass-through covered
MediumTestingExisting tests still pass (no regressions)Passcli-exec (Windows) re-run green; discovery too-large instrumentation preserved
MediumPerformanceNo N+1 queries or unbounded data fetchingPassPatterns compiled once (a3388a3) — avoids O(patterns) re-read per call
MediumPerformanceLong-running tasks use background jobsN/A
MediumQualityFollows existing codebase patternsPassnosemgrep justification matches repo convention; comments explain intent
MediumQualityChanges are focused (single concern)PassRedaction + ReDoS bound; PERCY_CHROMIUM_BASE_URL leg reverted out (794d0e0)
LowQualityMeaningful names, no dead codePass
LowQualityComments explain why, not whatPassRedaction contract + ReDoS rationale documented inline
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

No Critical or High findings.

Note (design characteristic, non-blocking): redaction is egress-scoped — it scrubs the copy sent to the Percy API. The local per-pid temp log file and terminal/CI console output are intentionally not rewritten. This matches the fix's stated scope (CWE-532 network egress); worth a one-line note in the ticket so the boundary is explicit.


Verdict: PASS

@pranavz28pranavz28 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.

LGTM

@Shivanshu-07
Shivanshu-07 merged commit 3f32984 into masterJul 17, 2026
64 of 65 checks passed
@Shivanshu-07
Shivanshu-07 deleted the security/cli-runtime-redact-redos-ssrf branch July 17, 2026 10:05
ninadbstack added a commit that referenced this pull request Sep 1, 2026
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>
Sign up for freeto 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.

3 participants

@Shivanshu-07@github-advanced-security@pranavz28