Skip to content

Reject app proxy requests with a stale or missing timestamp - #2091

Open
mwallba wants to merge 4 commits into
Shopify:mainfrom
effizient-io:app-proxy-timestamp-tolerance
Open

mwallba wants to merge 4 commits into
Shopify:mainfrom
effizient-io:app-proxy-timestamp-tolerance

Conversation

@mwallba

@mwallba mwallba commented Oct 19, 2026

Copy link
Copy Markdown

What this PR does

ShopifyApp::AppProxyVerification checks the HMAC signature of an app proxy request but never looks at its timestamp. Shopify signs every proxy request with the current time, so a signed proxy URL never expires: anyone who gets hold of one (from request logs, an APM or error tracker, a log pipeline) can replay it against the app for as long as they like, acting as the logged_in_customer_id it carries. Since the signature covers only the query parameters, the same query string also works on the app's other proxy paths.

@shopify/shopify-api already guards against this: validateHmac rejects app proxy requests whose timestamp is more than 90 seconds from now (HMAC_TIMESTAMP_PERMITTED_CLOCK_TOLERANCE_SEC in lib/utils/hmac-validator.ts). This PR brings the Ruby gem in line:

  • Requests whose timestamp is missing, not a single integer, or more than 90 seconds from the current time (in either direction) are rejected with 403, after the signature check.
  • The tolerance is configurable with config.app_proxy_timestamp_tolerance (seconds, default 90). Setting it to nil disables the check.
  • The existing tests now run at the time their signed fixtures were created (travel_to), so they keep testing the signature as before.

I came across this after seeing unusual requests to our own app's proxy endpoints: within seconds of a genuine storefront request, the same signed URL was sent directly to our server, once unchanged (accepted) and once with an altered signature (rejected). The signature check did its job, but the unchanged replay went through, and it would have kept working indefinitely. We've deployed the same check in our app. Tested through a development store's storefront, Shopify's own proxy requests arrived with timestamps only a few seconds old, well inside the tolerance.

Reviewer's guide to testing

  1. bundle exec rake test TEST=test/shopify_app/controller_concerns/app_proxy_verification_test.rb. The new tests cover:
    • ±90 seconds is accepted, ±91 seconds is rejected
    • a missing, blank, non-integer or repeated timestamp is rejected
    • a custom tolerance, and nil disabling the check
    • an end-to-end replay: the same signed request is served after 60 seconds and gets a 403 after 5 minutes
  2. bundle exec rake test TEST=test/shopify_app/configuration_test.rb for the default and the setter.
  3. In an app with an app proxy, requests through the storefront keep working, and a proxy URL copied from the logs and replayed a few minutes later gets a 403.

Things to focus on

  1. Default-on vs opt-in. I made the check default-on to match @shopify/shopify-api and so existing apps are protected without changes. Requests forwarded by Shopify are unaffected, but apps whose own tests sign proxy requests without a timestamp will see those tests fail, so I marked it ⚠️ [Breaking] in the changelog and added an Upgrading entry. Happy to make it opt-in instead, or to land it behind a different default, if you'd rather ship it in a minor release.
  2. The option's name and type. app_proxy_timestamp_tolerance in plain seconds, nil to disable. Open to a different name or an ActiveSupport::Duration.
  3. Strict parsing. Only a single all-digit timestamp is accepted, mirroring the hardening in Shopify/shopify-app-js (d282da59): decimal, negative, exponent and repeated values are rejected.

Checklist

  • Update CHANGELOG.md if the changes would impact users
  • Update README.md, if appropriate. (not needed: the README doesn't cover app proxies)
  • Update any relevant pages in /docs, if necessary (docs/Upgrading.md, docs/shopify_app/engine.md)
  • For security fixes, the Disclosure Policy must be followed. Opened as a public PR: the equivalent check is already public in @shopify/shopify-api, and exploiting the gap requires access to an app's own logs or infrastructure.

The signed query strings in these tests were created at 2016-06-16
19:41:23 UTC. Running the tests at that time keeps them valid once the
verification takes the request's timestamp into account.
Shopify signs every app proxy request with the current time, but
AppProxyVerification only checked the signature, so a signed proxy URL
stayed valid forever and could be replayed. Requests whose timestamp is
missing, not a single integer, or more than 90 seconds from now are now
rejected with 403, matching the tolerance in @shopify/shopify-api.

The tolerance is configurable through app_proxy_timestamp_tolerance
(seconds); nil disables the check.
Adds a breaking-change entry to the changelog, an upgrade note with the
reason and the migration steps, and a paragraph on the timestamp check and
app_proxy_timestamp_tolerance to the app proxy section of the engine docs.
@github-actions github-actions Bot added cla-needed devtools-gardener Post the issue or PR to Slack for the gardener labels Oct 19, 2026
@mwallba

mwallba commented Oct 19, 2026

Copy link
Copy Markdown
Author

I have signed the CLA!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devtools-gardener Post the issue or PR to Slack for the gardener

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant