[VUL-471] Fix arbitrary file read vulnerability in web CLI push config commands#311
[VUL-471] Fix arbitrary file read vulnerability in web CLI push config commands#311umair-ably wants to merge 1 commit intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR fixes an arbitrary file read vulnerability in the web CLI where an authenticated user could pass a non-certificate path (e.g., Changes
Review Notes
|
Block push:config:set-apns and push:config:set-fcm in web CLI mode since they read files from the server filesystem (not the user's local machine). Also block --control-host in web CLI mode to prevent data exfiltration, and add file extension validation (.p12/.pfx/.p8/.json) as defense in depth.
de6d57b to
a4672c6
Compare
There was a problem hiding this comment.
Review Summary
This PR fixes a real security vulnerability (arbitrary file read in web CLI) with a layered defense approach. The implementation is correct and the architecture is sound. One genuine gap and one observation:
Issue 1 — Missing test coverage for push:config:set-fcm web CLI restriction (minor gap)
The PR adds push:config:set-fcm to WEB_CLI_RESTRICTED_COMMANDS but there's no corresponding test in test/unit/commands/push/config/set-fcm.test.ts verifying the restriction works. Every other new behavior in this PR has a test — this one doesn't.
Risk: If set-fcm is accidentally dropped from the restricted list in future, there's nothing to catch it.
Suggested fix: Add a describe("web CLI restrictions", ...) block to set-fcm.test.ts mirroring the one added to set-apns.test.ts. The pattern is already there in the other file.
Observation — --control-host guard placement is correct but non-obvious
The --control-host check is inside createControlApi(), which runs at API-call time (inside run()), not at command startup. This is fine because:
- File-reading commands (
set-apns,set-fcm) are blocked earlier bycheckWebCliRestrictions()ininit(), so they never reachrun() - For non-file-reading commands like
push:config:show, thecreateControlApi()guard is the right layer
The show.test.ts test correctly exercises this path. No change needed — just documenting that the ordering is intentional.
Everything else looks correct
WEB_CLI_RESTRICTED_COMMANDSentries are correct — both file-reading commands blocked- Extension validation (
path.extname()+path.resolve()) correctly catches extensionless paths (returns""→ shown as"(no extension)") and wrong extensions this.fail()after extension check returnsnever, so the subsequentfs.existsSync()is unreachable on invalid extensions — no double-read risk- FCM command uses JSON structure validation (
type: "service_account"+project_idcheck) as its content guard, which is more meaningful than extension checking for JSON files - Test for
.pfxacceptance correctly verifies the "passes extension, fails file-not-found" path accessTokencheck before--control-hostcheck increateControlApi()is fine — web CLI always has a token set
Verdict: Ready to merge after adding the missing FCM restriction test.
There was a problem hiding this comment.
Review Summary
This PR fixes a real security vulnerability with a correct layered defense approach. One genuine gap worth addressing before merge:
Issue: Missing test for push:config:set-fcm web CLI restriction
push:config:set-fcm is added to WEB_CLI_RESTRICTED_COMMANDS but set-fcm.test.ts has no test verifying this restriction works. Every other new behavior in this PR has test coverage — this one does not.
If set-fcm is accidentally removed from the restricted list in a future refactor, nothing will catch the regression.
Suggested fix: add a describe("web CLI restrictions") block to set-fcm.test.ts mirroring the one added to set-apns.test.ts.
Observations (no action needed)
The --control-host guard in createControlApi() is placed correctly. File-reading commands are blocked earlier by checkWebCliRestrictions() in init() before run() is called, so they never reach createControlApi(). For non-file-reading Control API commands like push:config:show, createControlApi() is the right interception point — show.test.ts covers this.
Extension validation is correct. path.extname("/etc/passwd") returns "", which is caught and shown as "(no extension)". Since this.fail() returns never, the fs.existsSync() call below is unreachable on invalid extensions — no double-read risk.
FCM's defense-in-depth is structural, not extension-based. The JSON validation (type: "service_account" + project_id check) is a stronger content guard for JSON files than an extension check would be, and is consistent with what FCM actually requires.
Verdict: ready to merge after adding the missing FCM web CLI restriction test.
|
@claude I amended the commit and added a test for push:config:set-fcm web CLI restriction |
There was a problem hiding this comment.
Review Summary
Security fix for VUL-471. The implementation is correct and the defense-in-depth approach is well-structured. No bugs or correctness issues found.
What the PR does (correctly)
Layer 1 — Command blocking (primary fix): Adding push:config:set-apns and push:config:set-fcm to WEB_CLI_RESTRICTED_COMMANDS is the right primary fix. checkWebCliRestrictions() fires in init() before run() executes, so the command never reaches the file-reading code in web CLI mode.
Layer 2 — --control-host blocking (secondary fix): Blocking --control-host in createControlApi() is a good safety net for all Control API commands, not just the file-reading ones. Because createControlApi is always called before any API request, this ensures no other Control API command can redirect file content to an attacker-controlled host in web CLI mode. The ABLY_CONTROL_HOST env var path is also covered since oclif populates flags["control-host"] from it.
Layer 3 — Extension validation (defense in depth): Restricting to .p12/.pfx/.p8/.json limits the attack surface for local CLI users. It's correctly placed before existsSync so the cheaper check runs first. Extension checks use .toLowerCase() to handle upper-case variants.
One necessary test fix the PR correctly handles
The old set-fcm.test.ts test for "should fail when service account file is not valid JSON" used test-apns-key.p8 as the input — after adding extension validation, this test would still pass (error is defined) but for the wrong reason (extension rejection, not JSON parsing failure). The PR correctly replaces it with a temp .json file containing invalid JSON content and adds expect(error?.message).toContain("not valid JSON") to pin the right failure mode.
Similarly, the three existing set-apns.test.ts tests that passed p8FixturePath to --certificate are correctly updated to use the new p12FixturePath fixture, since .p8 would now be rejected.
No issues found
- Error handling uses
this.fail()throughout — correct - Component strings (
"pushConfigSetApns","pushConfigSetFcm","auth") are camelCase — correct beforeEach/afterEachenv var save/restore pattern in web CLI tests is correct- The
"not available in the web CLI"substring in test assertions correctly matches the default message fromcheckWebCliRestrictions()
LGTM.
Summary
push:config:set-apnsandpush:config:set-fcmin web CLI mode — these commands read files from the server filesystem (not the user's local machine) and have no legitimate web CLI use case since there's no file upload mechanism--control-hostflag in web CLI mode to prevent redirecting API requests (and file contents) to attacker-controlled servers.p12/.pfxfor certificates,.p8for keys) as defense in depth against arbitrary file readsContext: A security researcher demonstrated that an authenticated web CLI user could read arbitrary server container files (e.g.,
/etc/passwd) via--certificate /etc/passwdand exfiltrate them using--control-host https://attacker.com. The local CLI is not affected — local users already have full filesystem access.Test plan
pnpm preparepassespnpm exec eslint .— 0 errorspnpm test:unit— 2258 tests passpush:config:set-apnsis rejected in web CLI mode (ABLY_WEB_CLI_MODE=true)--control-hostis rejected for any control API command in web CLI mode--certificate /etc/passwd)🤖 Generated with Claude Code