fix(core): allowlist the /percy/log level so request input never drives dynamic dispatch (PER-8625) - #2426
Conversation
…es dynamic dispatch (PER-8625)
POST /percy/log used the request-supplied `level` as a property key on the
logger group object (`log[level](message, meta)`). On an unauthenticated
local endpoint that let any caller invoke arbitrary logger methods, e.g.
`{"level":"loglevel","message":"debug"}` flipped the process-wide log level
and `deprecated` emitted arbitrary warnings. This is the chain-breaker
finding (PER-8606, CWE-1321) for the PER-8625 security chain.
Validate `level` against debug/info/warn/error, respond 400 otherwise, and
dispatch through an explicit switch so no request input ever reaches a
dynamic property lookup.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Central YAML (base), Workspace UI (inherited) Review profile: ASSERTIVE Plan: Enterprise Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
ninadbstack
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.
| } | ||
|
|
||
| // Log levels an SDK may forward through POST /percy/log. | ||
| const SDK_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error']); |
There was a problem hiding this comment.
[Low] Level allowlist duplicates the logger's own level list
This set repeats the levels already defined as LOG_LEVELS in packages/logger/src/logger.js, which is not exported. If a level is ever added or renamed there without updating this copy, the two drift silently and this endpoint starts rejecting a valid level.
Suggestion: Export the level list from @percy/logger and import it here rather than keeping a second hardcoded copy. Fine as a follow-up.
Reviewer: stack-code-reviewer
| return res.json(400, { error: 'Invalid log level' }); | ||
| } | ||
|
|
||
| switch (level) { |
There was a problem hiding this comment.
[Low] Switch is redundant once the guard has run
After SDK_LOG_LEVELS.has(level) passes, level is provably one of four safe literals, so log[level](message, meta) would be equally safe and shorter. The comment above reads as though the guard alone is the mitigation, which leaves this switch looking redundant to a later reader.
Suggestion: Keep the switch if it is deliberate defense in depth against any dynamic property access, and say so in the comment. Otherwise collapse it back to the guarded dynamic call.
Reviewer: stack-code-reviewer
Claude Code PR ReviewPR: #2426 • Head: fec8de0 • Reviewers: stack-code-reviewer SummaryAllowlists the four real log levels on the unauthenticated Review Table
Findings
The other human reviewer approved this PR without inline comments, so there were no human concerns to confirm or carry forward. Verdict: PASS — the security fix is correct and well covered by tests; all three findings are Low and non-gating. |
Summary
POST /percy/logused the request-suppliedlevelas a property key on the logger group object:log[level](message, meta).{"level":"loglevel","message":"debug"}returned 200 and flipped the process-wide log level;{"level":"deprecated",...}emitted arbitrary deprecation warnings.__proto__/constructorproduced a 500 rather than pollution, but the primitive is the same one the ticket asks us to close.levelis now validated againstdebug|info|warn|errorand rejected with 400{ error: 'Invalid log level' }otherwise. Dispatch is an explicitswitch, so no request input reaches a dynamic property lookup.@percy/sdk-utilsonly ever sends the four valid levels.Test plan
__proto__,constructor,loglevel,deprecated,stdout,'',null,42) gets a 400, the global loglevel is untouched, nothing is logged,Object.prototypeis clean. Verified this spec fails against the unpatched handler.switchat 100% coverage)./logspec and the fullAPI Serverdescribe block pass locally (39 specs);eslintclean.🤖 Generated with Claude Code