security: scope CORS to loopback + block cross-origin config/stop (PER-8600/8601/8602/8603)#2282
Open
Shivanshu-07 wants to merge 1 commit into
Open
security: scope CORS to loopback + block cross-origin config/stop (PER-8600/8601/8602/8603)#2282Shivanshu-07 wants to merge 1 commit into
Shivanshu-07 wants to merge 1 commit into
Conversation
…R-8600/8601/8602/8603) The local Percy server is unauthenticated by design (SDKs post to it), but it served `Access-Control-Allow-Origin: *` and accepted state-changing requests from any origin, so a malicious website the user was visiting could read responses from, mutate the live config of, or stop the local server via the browser. PER-8602 (CWE-942) — replace the wildcard ACAO with loopback-origin-only reflection in the server CORS middleware (isLoopbackOrigin). Non-loopback origins receive no ACAO, so arbitrary sites can no longer read local-server responses. Node SDK clients don't use CORS and are unaffected; loopback browser tooling (any localhost port) still works. PER-8600 / PER-8601 (CWE-306/CWE-352) — add assertNotCrossOrigin() and apply it to the state-changing endpoints POST /percy/config and /percy/stop: a request carrying a non-loopback Origin header is rejected (403). Node SDK clients send no Origin and are unaffected. PER-8603 (CWE-915, mass assignment) — already mitigated by the existing stripBlockedConfigFields (httpReadOnly) control on /percy/config; the cross-origin guard further prevents unauthenticated browser-driven mutation. Host-based validation was deliberately NOT added: the server binds to all interfaces by default (PERCY_SERVER_HOST, default `::`), so Dockerized SDKs legitimately reach it via a non-loopback Host. Origin-based checks defend the browser attack vector without breaking those setups. Adds unit tests for the cross-origin config/stop rejection and loopback allow. NOTE: fully authenticating the local API (a shared token adopted by every SDK) remains a larger coordinated change; this PR closes the browser-origin vector that the findings (and chain PER-8626) actually exploit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| let origin = req.headers.origin; | ||
| let originAllowed = isLoopbackOrigin(origin); | ||
| if (originAllowed) { | ||
| res.setHeader('Access-Control-Allow-Origin', origin); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fifth (final contained) percy-cli security PR — the local-server origin findings (deadline 2026-06-16) + chain PER-8626.
POST /percy/configThe constraint
The local API is unauthenticated by design — every SDK posts to it without credentials. The real exploit path in these findings is a malicious website the user is visiting reaching
http://localhost:5338from the browser. The fix defends that vector without breaking the SDK contract.Changes
server.js(PER-8602): the CORS middleware no longer returnsAccess-Control-Allow-Origin: *. It reflects the requestOriginonly when it's loopback (isLoopbackOrigin—localhost/127.0.0.1/::1/*.localhost, any port). Arbitrary sites get no ACAO and can't read local-server responses (build data, config). Node SDK clients don't use CORS; loopback browser tooling (e.g. the Storybook manager onlocalhost:6006) still works.api.js(PER-8600 / PER-8601):assertNotCrossOrigin()rejects (403) anyPOST /percy/configor/percy/stoprequest carrying a non-loopbackOriginheader. Node SDK clients send noOrigin→ unaffected; a malicious site's cross-originfetch→ blocked.PER-8603: already mitigated by the existing
stripBlockedConfigFields(httpReadOnly) control; the cross-origin guard adds defense against browser-driven mutation.Why not Host-validation
The server binds to all interfaces by default (
PERCY_SERVER_HOST, default::), so Dockerized SDKs legitimately reach it via a non-loopbackHost. Origin-based checks defend the browser vector without breaking those setups.Verification
isLoopbackOriginverified (localhost/127.0.0.1/*.localhostallowed;evil.com/empty/undefined rejected); both filesnode --checkclean./configPOST rejected + config unmutated; loopback/configPOST allowed; cross-origin/stoprejected +percy.stopnot called.Origin) → unaffected.Closes PER-8602; closes the browser-origin vector for PER-8600/8601/8603 and chain PER-8626.
🤖 Generated with Claude Code