Skip to content

[VUL-506] fix(push): stop web CLI from reading server-side files via payload args#407

Merged
umair-ably merged 2 commits into
mainfrom
fix/web-cli-push-payload-file-read
Jun 5, 2026
Merged

[VUL-506] fix(push): stop web CLI from reading server-side files via payload args#407
umair-ably merged 2 commits into
mainfrom
fix/web-cli-push-payload-file-read

Conversation

@umair-ably

@umair-ably umair-ably commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Author's Note

Adds a new resolveJsonInput function which blocks @file params in the web cli only. This allows us to still keep certain push commands in the web-cli e.g. push publish which is still useful functionality given a user could've configured their push env in the dashboard first. The alternative was a blanket block of any and all file uploading commands which would result in a worse web-cli experience than necessary.

Summary

push publish, push batch-publish, and push devices save accept a JSON payload that may also be a path to a local file (@file, a /·./·../ path prefix, or a bare path that exists on disk), which is loaded via fs.readFileSync. In web CLI mode the filesystem is the shared terminal-server container rather than the user's machine, so loading a file there reads server-side contents.

This mirrors the existing restriction already applied to push config set-fcm/set-apns, extending it to the remaining payload-loading commands.

Change

  • Add AblyBaseCommand.resolveJsonInput(input, label, flags, component) — a single helper for JSON-or-file inputs:
    • Local CLI mode: reads @file/path inputs from disk (unchanged behaviour).
    • Web CLI mode: never touches the filesystem — input is treated as literal JSON, and an @file reference is rejected with a clear message.
  • Route push publish (--payload), push batch-publish (payload arg), and push devices save (--data) through the helper instead of calling fs.readFileSync on flag/arg values directly.
  • Any future file-reading command should use this helper so the restriction is inherited rather than re-implemented per command (skill doc updated to reflect this).

The commands remain fully usable in the web CLI with inline JSON — only the server-side file-loading shortcut is removed there.

Tests

  • Per-command coverage asserting the server filesystem is not read in web CLI mode, that @file references are rejected, and that local-mode file loading still works.
  • pnpm prepare, eslint (0 errors), and the affected unit tests pass.

🤖 Generated with Claude Code

push publish, batch-publish, and devices save accept a JSON payload that
may also be a path to a local file (@file, a path prefix, or a bare path
that exists), loaded via fs.readFileSync. In web CLI mode the filesystem
is the shared terminal-server container rather than the user's machine,
so loading a file there exposes server-side contents.

Route all JSON-or-file payload inputs through a new
AblyBaseCommand.resolveJsonInput() helper. It only reads from disk in
local CLI mode; in web CLI mode the input is always treated as literal
data and an @file reference is rejected with a clear message. The
commands stay fully usable in the web CLI with inline JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Jun 4, 2026 10:25am

Request Review

@umair-ably umair-ably marked this pull request as ready for review June 4, 2026 10:09
@umair-ably umair-ably changed the title fix(push): stop web CLI from reading server-side files via payload args [VUL-506] fix(push): stop web CLI from reading server-side files via payload args Jun 4, 2026
@claude-code-ably-assistant

Copy link
Copy Markdown

Walkthrough

This PR fixes a server-side file-read vulnerability (VUL-506) in the web CLI. Three push commands (push publish, push batch-publish, push devices save) accepted JSON-or-file inputs and called fs.readFileSync directly — in web CLI mode this reads from the shared terminal-server container rather than the user's machine, potentially exposing server-side contents. The fix introduces a single resolveJsonInput() helper on AblyBaseCommand that enforces the web-CLI restriction in one place, so future commands inherit it automatically.

Changes

Area Files Summary
Commands src/commands/push/publish.ts, src/commands/push/batch-publish.ts, src/commands/push/devices/save.ts Remove inline fs.readFileSync logic; route all JSON-or-file inputs through the new resolveJsonInput() helper
Base Class src/base-command.ts Add protected resolveJsonInput(input, label, flags, component) — reads files in local mode, rejects @file refs and skips filesystem in web CLI mode
Tests test/unit/commands/push/publish.test.ts, test/unit/commands/push/batch-publish.test.ts, test/unit/commands/push/devices/save.test.ts Add web CLI file-read restriction describe blocks per command: assert server filesystem is not read in web CLI mode, @file refs are rejected, and local-mode file loading still works
Docs / Skills .claude/skills/ably-new-command/SKILL.md Expand the security rule to describe both the block-the-command and disable-just-the-file-read approaches, with guidance to always use resolveJsonInput rather than raw fs.readFileSync

Review Notes

  • Security fix — this is a path-traversal / server-file-disclosure issue in web CLI mode. The change is additive: local CLI behaviour is unchanged; only web CLI mode gains the restriction.
  • resolveJsonInput is now the required pattern for any flag/arg that may be a file path. The skill doc update documents this; future commands that call fs.readFileSync directly on user input would reintroduce the same class of bug.
  • Bare path detection (fs.existsSync(path.resolve(input)) fallback in local mode) is preserved from the original implementations — this is intentional to keep existing behaviour, but reviewers should confirm they're comfortable with this implicit file-detection heuristic.
  • Test isolation — tests toggle ABLY_WEB_CLI_MODE via process.env with beforeEach/afterEach teardown; temp files use os.tmpdir() with PID-namespaced names to avoid cross-test collisions.
  • No new dependencies — uses only node:fs and node:path, both already available; imports moved from command files up to base-command.ts.

@claude-code-ably-assistant claude-code-ably-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: fix(push) – stop web CLI from reading server-side files via payload args

Overview

This PR fixes VUL-506 by introducing resolveJsonInput in AblyBaseCommand, centralising the JSON-or-file logic so the web CLI restriction is enforced in one place rather than repeated (and potentially missed) per command. The security fix is correct and the approach is clean.


One real issue

Bare path inputs in web CLI mode give a misleading error

In web CLI mode, resolveJsonInput only explicitly rejects the @ prefix:

if (this.isWebCliMode) {
  if (input.startsWith("@")) {
    this.fail("Loading ... from a file is not supported in the web CLI...");
  }
  return input;  // /path/to/file, ./file.json, etc. fall through silently
}

A web CLI user who passes --payload ./push.json gets Invalid JSON for --payload: ./push.json from the downstream parseJsonObjectFlag, not the helpful "not supported in the web CLI" message. Secure? Yes — no file is ever read. Confusing? Yes — they have no idea why their path didn't work.

Since the path-prefix detection already exists in the local branch, the web CLI guard is easy to extend:

if (this.isWebCliMode) {
  const looksLikePath =
    input.startsWith("@") ||
    input.startsWith("/") ||
    input.startsWith("./") ||
    input.startsWith("../");
  if (looksLikePath) {
    this.fail(
      `Loading ${inputLabel} from a file is not supported in the web CLI. Pass the JSON inline instead.`,
      flags,
      component,
    );
  }
  return input;
}

Minor observation (not a blocker)

Expanded behaviour for push devices save --data

The original parseDeviceData only recognised @file. Via resolveJsonInput, --data now also accepts /absolute/path, ./relative/path, and bare paths that exist on disk in local CLI mode. That's arguably a nice improvement, but it isn't mentioned and has no test coverage. Not a problem, just worth noting in case it surprises anyone auditing the flag behaviour.


What's good

  • The centralised helper is the right architectural call — future commands get the restriction for free, and the skill doc now points to it clearly.
  • All three scenarios (local file load, web CLI bare path treated as JSON, web CLI @file rejection) are covered by tests in all three affected commands.
  • this.fail() returns never, so the @ check in web CLI mode has no fall-through risk.
  • BaseFlags carries [key: string]: unknown, so Record<string, unknown> from parseDataFlag is compatible without a cast.

The path-prefix UX gap is worth fixing before merge, but the security property itself is solid.

Previously only an @file reference produced the "not supported in the web
CLI" error; a bare path like ./push.json fell through and surfaced as a
confusing "Invalid JSON" error from the downstream parser. Extend the web
CLI guard in resolveJsonInput to reject any file reference (@file or a /,
./, ../ path prefix) with the clear message. Detection stays prefix-based
so the helper never probes the server filesystem via existsSync.

Also add test coverage for push devices save --data accepting a local
path input (not just @file) in local CLI mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@umair-ably umair-ably merged commit 58476a8 into main Jun 5, 2026
11 checks passed
@umair-ably umair-ably deleted the fix/web-cli-push-payload-file-read branch June 5, 2026 13:57
@umair-ably umair-ably mentioned this pull request Jun 5, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants