Embedded app hmac fails verification when a request has nested (bracket) query params — delivered param order differs from the order used to sign

Hello!

I ran into this issue while clearing up some HMAC validation failures, in particular when the URL contains some additional query parameters for filtering/sorting etc, that are unpacked as arrays/hashes.

Summary

When an embedded app is loaded with nested/bracket query parameters (e.g. foo[a]=…&foo[b]=…) whose keys are not in alphabetical order, the hmac query parameter cannot be verified by the app. Shopify computes the HMAC using the parameters in their original (insertion) order, but the iframe URL Shopify delivers to the app has the nested keys re-sorted alphabetically. Since the app only ever sees the delivered (re-sorted) URL, it cannot reconstruct the exact message that was signed, and HMAC verification fails for a perfectly legitimate request.

Environment

  • Embedded app loaded inside Shopify admin (App Bridge); standard request hmac query-param verification (not the session token / id_token).

  • Reproduced on a development store.

Steps to reproduce

  1. From within Shopify admin, navigate to an embedded app deep link whose query string contains a nested/bracket param with keys in non-alphabetical order. Example (a date-range filter):

    …/apps/<app>/<path>?page=1&startedAt[before]=2026-06-02 21:00:00&startedAt[after]=2026-05-31 21:00:00
    

    (note: before precedes after)

  2. Shopify renders the embedded app iframe with a signed URL. The nested keys are re-ordered alphabetically in the delivered URL:

    https://<app-host>/<path>?embedded=1&hmac=<hmac>&host=<host>&id_token=<jwt>&locale=en&page=1&session=<session>&shop=<shop>.myshopify.com&startedAt[after]=2026-05-31+21%3A00%3A00&startedAt[before]=2026-06-02+21%3A00%3A00&timestamp=<ts>
    

    (now after precedes before)

  3. In the app, verify hmac by reconstructing the signed message from the received params, per the standard process.

Expected

The HMAC verifies — the parameters delivered to the app reproduce the HMAC that was signed.

Actual

The HMAC does not verify. Brute-forcing the message construction against the received hmac (HMAC-SHA256 with our app secret) shows the only message that reproduces the signature uses the nested hash in its original order:

startedAt={"before" => "2026-06-02 21:00:00", "after" => "2026-05-31 21:00:00"}

i.e. the order from step 1 — not the alphabetical order delivered in step 2. (All values are otherwise correct: URL-decoded, with literal space and colon.)

Confirmation

Repeating step 1 with the keys already in alphabetical order (startedAt[after] before startedAt[before]) makes the delivered order equal the signed order, and verification succeeds. So the only variable is the ordering of the nested keys.

Likely cause

The HMAC message reflects the parsed structure’s natural (insertion) ordering, while the URL delivered to the app serializes nested params with keys sorted alphabetically (e.g. Hash#to_query sorts keys, whereas the value interpolated into the signed message uses the hash’s insertion order). The two diverge whenever the original key order is non-alphabetical.

Impact

Embedded apps cannot authenticate legitimate requests that carry nested/bracket query params in non-alphabetical order — common for range filters (before/after), multi-field filters, sort specs, etc. Affected merchants hit a failed auth / blank app screen on those deep links, with no app-side remedy (the signing order is unrecoverable from the delivered URL).

Can I know when does this happen? Which app having the issue?

Hi Henry!

This happens after a successful initial load of our app, on browser refresh.

For example, once our app is loaded and the user adds some filters in our app, we add those to the top-level (window) url as query parameters.

Upon a hard refresh of the Shopify Admin, our query parameters are also taken into account for the HMAC computation, however for a case such as above, with a date filter, represented as date[before] and date[after], the order is not preserved, it seems to sort the before and after keys alphabetically, between the admin.shopify.com request and the iframe request to our app.

I mean it just happened today or existing bug. I tried to scope down the changes.

It seems to be an existing bug.

Please bear with me as I try to explain.

We always had issues with validating HMAC with nested parameters like date[after] etc.

Those are, seemingly, represented using a Ruby Hash.to_s in Shopify backend, and encoded into the string over which HMAC is generated, as date={“after” => “<value>”} exactly, which is nowhere mentioned in the documentation, or at least I could not find it.

So this is an improvement point for the documentation.

After adjusting our HMAC verification to take this Ruby qurik into account, our issues in this area reduced, and I noticed this ordering quirk as well – from a real client in production, then reproduced it on my own dev store, and confirmed that ordering was at fault by swapping them around in the initial admin.shopify.com request (so that the keys were already sorted in the URL, e.g. admin.shopify.com/apps/<app>/<path>?embedded=1&...&date[after]=<value>&date[before]=<value>
instead of admin.shopify.com/apps/``<app>/<path>?embedded=1&...&date[before]=<value>&date[after]=<value>).

I think the action item here could be documentation.

Partly, yes. The ordering thing is an issue.

Either every app developer enforces the query parameters to be always sorted, or you guys dont re-order them in between the initial admin.shopify.com navigation and the iframe request to the app itself.

I’d lean towards the latter, Shopify should preserve the order used in the HMAC when forwarding the request to the app being loaded.

You can test this out rather easily by putting any query params like the ones above with the keys out of order, and see that it reaches the app’s iframe with the keys sorted.