Skip to content

Redact store signup JWTs from analytics payloads - #8444

Open
craigmichaelmartin wants to merge 1 commit into
mainfrom
craigmartin/redact-signup-jwt-from-analytics
Open

Redact store signup JWTs from analytics payloads#8444
craigmichaelmartin wants to merge 1 commit into
mainfrom
craigmartin/redact-signup-jwt-from-analytics

Conversation

@craigmichaelmartin

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Related to the internal security finding tracked as Vault 69736 (cli / oauth-client).

shopify store stripe-auth accepts a signup JWT via --signup. That JWT is a bearer credential for the target store, and the Stripe Projects setup guide that drives this command classifies it as a server secret that must never be printed.

Command arguments are reported to Monorail verbatim as sensitive.args (prerun.tsstartAnalytics({args})args: startArgs.join(' ')), and sanitizePayload only knew about Theme Access passwords and --store-password. So the credential left the machine on every invocation that passes the flag — not only on the fallback path where the URL is printed.

Reproduced before the change with a fake token and analytics delivery disabled:

$ SHOPIFY_CLI_NO_ANALYTICS=1 node packages/cli/bin/dev.js store stripe-auth \
    --store "not a real store!!" --scopes read_products \
    --signup "eyJTOTALLYFAKE.NOTAREALJWT.SIGSEG" --verbose
...
"args": "--store not a real store!! --scopes read_products --signup eyJTOTALLYFAKE.NOTAREALJWT.SIGSEG --verbose"

How this relates to the other two PRs. This is complementary, not a rival fix — there is no file overlap.

Sink Owner
#8427 terminal output when the browser can't open @dengjeffrey
#8428 openURL argv, and the CLI's own argv for stdin callers @dengjeffrey
this PR analytics payload sent to Monorail

Both of those are confined to packages/store; this sink is in packages/cli-kit, so it survives either of them landing. Any merge order works. This is a sink-side backstop and does not close the finding on its own — the credential still reaches the browser's authorization URL, and it still sits in the invoking shell's history and this process's own argv while --signup remains a documented flag.

WHAT is this pull request doing?

Adds three rules to sanitizePayload covering the shapes the payload can carry the credential in, mirroring the three that already exist for store-password:

  • a command-line flag value — --signup <jwt> and --signup=<jwt>
  • a JSON key — "signup": "<jwt>", as reached via cmd_all_environment_flags
  • a URL query parameter — signup=<jwt> inside a URL in error_message or metadata

Two deliberate choices worth flagging for review:

The bare-value alternative changed from [^\s"]+ to (?:\\.|[^\s"\\])+. Redaction runs on the serialized payload, so a value quoted on the command line arrives with its quotes escaped. The old class stops at neither, which had two bad outcomes I verified against the current rule:

argv current --store-password rule
--store-password "secret" JSON.parse throws inside sanitizePayload → whole event dropped
--store-password 'a"b' same

Simply excluding \\ would stop the throw but leak the value instead. Treating a JSON escape as part of the value handles both, so I applied it to the existing store-password rule too rather than leaving one correct rule next to a broken one. That is the only behaviour change outside signup, and it is covered by its own test.

The URL rule has a (?<![\w-]) guard. Without it, an unrelated from_signup=true would be masked. With it, https://partners.shopify.com/signup and from_signup= are untouched — both asserted in tests.

Known limitation, stated so the next reader doesn't over-trust it: this redaction is coupled to the parameter name. If core renames signup, the rules go stale with no test failure. That is inherent to sink-side redaction and is why the source-side fix in #8428 still matters — they are complements, not substitutes.

How to test your changes?

pnpm vitest run packages/cli-kit/src/public/node/analytics.test.ts

5 of the 6 new tests fail without the analytics.ts change; the sixth is the over-redaction guard, which passes both before and after by design.

End-to-end, with delivery disabled so nothing is transmitted:

pnpm nx build cli-kit   # bin/dev.js resolves cli-kit from dist
SHOPIFY_CLI_NO_ANALYTICS=1 node packages/cli/bin/dev.js store stripe-auth \
  --store "not a real store!!" --scopes read_products \
  --signup "eyJTOTALLYFAKE.NOTAREALJWT.SIGSEG" --verbose | grep '"args"'

Expect --signup *****, and zero occurrences of the token anywhere in the payload. Repeat with --signup=<jwt> to cover the other spelling.

Also run locally: eslint and prettier --check on both files clean, nx run cli-kit:type-check clean, pnpm knip clean, and the full packages/cli-kit suite at 1869 passed. The 2 failures in hooks/deprecations.test.ts reproduce on a clean main and are unrelated.

Post-release steps

None.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — string sanitization only, no platform-dependent behaviour
  • I've considered possible documentation changes — none; no command, flag, or output surface changes
  • I've considered analytics changes to measure impact — this is an analytics change: sensitive.args, cmd_all_environment_flags, error_message and metadata now mask signup values. No field is added or removed.
  • The change is user-facing — no changeset. Monorail payload contents are not user-observable and no command, flag, prompt, output or error behaviour changes; AGENTS.md scopes changesets to user-visible behaviour. Same call as 37e3ad3 "Prevent GitHub credentials from being logged", which shipped five files with no changeset.

`store stripe-auth` takes a signup JWT that is a bearer credential for the
target store. Command arguments are reported to Monorail verbatim in
`sensitive.args`, and `sanitizePayload` only knew about Theme Access passwords,
so every invocation sent the credential off the machine — including the happy
path where the browser opens and nothing is printed.

Redact the signup credential in the three shapes the payload can carry it:
as a command-line flag value, as a JSON key, and as a URL query parameter.

The bare-value alternative now treats a JSON escape as part of the value.
Applied to the existing store-password rule this also fixes a latent failure
where a quoted value produced a string that no longer parsed, which threw
inside `sanitizePayload` and dropped the whole event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assisted-By: devx/39092f35-5041-4a88-ab8d-808c98322495
@craigmichaelmartin
craigmichaelmartin requested a review from a team as a code owner August 31, 2026 16:38
@github-actions github-actions Bot added the no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant