fix: require authentication key for GraphiQL dev server#7033
fix: require authentication key for GraphiQL dev server#7033isaacroldan wants to merge 1 commit intomainfrom
Conversation
d754058 to
4890e97
Compare
Coverage report
Test suite run success3820 tests passing in 1476 suites. Report generated by 🧪jest coverage report action from 04702b0 |
4890e97 to
8d8da1f
Compare
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
8d8da1f to
e5c235d
Compare
There was a problem hiding this comment.
Pull request overview
This PR tightens access to the local GraphiQL dev server by ensuring an authentication key is always required (either user-provided or deterministically derived), and threads that key through the CLI/dev process and GraphiQL UI.
Changes:
- Derive a deterministic GraphiQL auth key from
apiSecret + storeFqdnwhen no key is provided, and always include it in the GraphiQL URL/process options. - Require the key for additional GraphiQL server endpoints and add basic
api_versioninput validation. - Add tests covering key derivation and API version validation, and update CLI flag description.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/app/src/cli/services/dev/processes/setup-dev-processes.ts | Always resolves a GraphiQL auth key (derive if absent) and uses it for the GraphiQL URL + process options. |
| packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts | Adds coverage to ensure a key is auto-derived and included in URL/process options. |
| packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx | Updates the in-browser status polling request to include the auth key. |
| packages/app/src/cli/services/dev/graphiql/server.ts | Derives/forces a key on the server, enforces key checks on more routes, and validates api_version before building Admin URLs. |
| packages/app/src/cli/services/dev/graphiql/server.test.ts | Adds unit tests for deriveGraphiQLKey and isValidApiVersion. |
| packages/app/src/cli/commands/app/dev.ts | Updates --graphiql-key help text to reflect the new default derived-key behavior. |
Comments suppressed due to low confidence (3)
packages/app/src/cli/services/dev/graphiql/server.ts:216
isValidApiVersioncurrently treats empty string as valid (if (!version) return true), but later/graphiql/graphql.jsonpasses that value intoadminUrl(storeFqdn, apiVersion).adminUrlusesversion ?? 'unstable', so an empty string will produce an invalid URL segment (.../api//graphql.json). Normalize''toundefinedbefore callingadminUrl(or makeisValidApiVersion('')return false and adjust callers/tests accordingly).
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Shopify-Access-Token': await token(),
'User-Agent': `ShopifyCLIGraphiQL/${CLI_KIT_VERSION}`,
}
return fetch(graphqlUrl, {
method: req.method,
packages/app/src/cli/services/dev/graphiql/server.ts:80
providedKey ?? deriveGraphiQLKey(...)will treat an empty string override as a real key. That effectively disables the protection (anyone can hit?key=) and also makes it easy to accidentally misconfigure via env/flag. Consider rejecting empty-string keys (e.g., trim + check length) and falling back to the derived key, or throw a clear error if a custom key is provided but empty.
function failIfUnmatchedKey(str: string, res: express.Response): boolean {
if (str === key) return false
res.status(404).send(`Invalid path ${res.req.originalUrl}`)
return true
}
let _token: string | undefined
async function token(): Promise<string> {
packages/app/src/cli/services/dev/graphiql/server.test.ts:53
- The tests assert that
isValidApiVersion('')is valid, but if an emptyapi_versionquery param slips through,adminUrl(storeFqdn, '')will generate an invalid.../api//graphql.jsonURL. Once the implementation is fixed (normalize''toundefinedor reject it), update this test expectation accordingly to match the real behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/app/src/cli/services/dev/processes/setup-dev-processes.ts
Outdated
Show resolved
Hide resolved
packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx
Outdated
Show resolved
Hide resolved
packages/app/src/cli/services/dev/processes/setup-dev-processes.ts
Outdated
Show resolved
Hide resolved
2415592 to
eb44a92
Compare
dmerand
left a comment
There was a problem hiding this comment.
I'm aligned with the approach. The agents had some good security suggestions worth considering, I've left notes here.
| const resolvedGraphiqlKey = resolveGraphiQLKey(graphiqlKey, apiSecret, storeFqdn) | ||
| const graphiqlURL = shouldRenderGraphiQL | ||
| ? `http://localhost:${graphiqlPort}/graphiql${graphiqlKey ? `?key=${graphiqlKey}` : ''}` | ||
| ? `http://localhost:${graphiqlPort}/graphiql?key=${resolvedGraphiqlKey}` |
There was a problem hiding this comment.
🐛 Bug: The resolvedGraphiqlKey may contain special characters if provided by the user via flags. Injecting it directly into the query string without encoding can result in a malformed URL or unexpected behavior.
Suggestion: ? http://localhost:${graphiqlPort}/graphiql?key=${encodeURIComponent(resolvedGraphiqlKey)}
7ce3874 to
d5bb5f8
Compare
The GraphiQL development server that ships with Shopify CLI proxied requests to the Shopify Admin API without authentication by default. Any process on the developer's machine could send requests to localhost:3457 and have them forwarded with a valid OAuth token. This change fixes three vulnerabilities: 1. Always require an auth key. When --graphiql-key is not provided, derive one deterministically via HMAC-SHA256(apiSecret, storeFqdn). This is stable across restarts (browser tabs survive) and not guessable without the app secret (which already grants full access). 2. Add failIfUnmatchedKey() to /graphiql/status endpoint, which previously leaked storeFqdn, appName, and appUrl even when a key was explicitly configured. 3. Validate api_version parameter to prevent path traversal attacks (e.g. ../rest/2024-01/products.json). Only YYYY-MM and 'unstable' are accepted. Bug bounty report: https://bugbounty.shopify.io/reports/3596212 Co-authored-by: Isaac Roldán <isaac.roldan@shopify.com> Co-authored-by: Claude Code <claude-code@anthropic.com>
d5bb5f8 to
04702b0
Compare

WHY are these changes introduced?
GraphiQL authentication keys were previously optional, requiring manual specification for security. This created a poor developer experience where keys had to be manually managed and could be lost across dev server restarts.
WHAT is this pull request doing?
--graphiql-keyflag description to reflect that keys are now automatically generated by default/graphiql/statusendpointThe derived keys are deterministic (same across dev server restarts) but not guessable without the app secret, providing both security and a seamless developer experience.
How to test your changes?
--graphiql-key--graphiql-keystill works as expectedMeasuring impact
How do we know this change was effective? Please choose one:
Checklist