sanitizeHost throws instead of returning null for base64-shaped hosts that don't decode to a URL
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 84/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- api, backend-api-design
Research direction
Start in packages/apps/shopify-api/lib/utils/shop-validator.ts and run the provided repro.mjs with the four host values. Trace sanitizeHost through decodeHost and URL parsing, then add coverage for base64-shaped invalid inputs if the project’s tests provide a suitable location. Done means invalid hosts return null, while throwOnInvalid consistently produces InvalidHostError instead of leaking decode or URL exceptions.
Written by the indexing model from the issue text.
Description
Issue summary
Before opening this issue, I have:
- Upgraded to the latest version of the relevant packages
@shopify/*package and version:@shopify/shopify-api14.0.0 (via@shopify/shopify-app-react-router2.0.0). Also verified against the published@shopify/shopify-api@14.0.1and againstmain— the code is unchanged in both.- Node version: v24.19.0
- Operating system: macOS 26.6.2
- Set
{ logger: { level: LogSeverity.Debug } }in my configuration, when applicable - Found a reliable way to reproduce the problem that indicates it's a problem with the package
- Looked for similar issues in this repository
- Checked that this isn't an issue with a Shopify API
sanitizeHost throws instead of returning null when host passes the base64 character check but does not decode into something new URL() accepts.
packages/apps/shopify-api/lib/utils/shop-validator.ts:
let sanitizedHost = base64regex.test(host) ? host : null;
if (sanitizedHost) {
const {hostname} = new URL(`https://${decodeHost(sanitizedHost)}`);
...
}
Neither decodeHost (which is atob) nor the URL constructor is guarded. The function returns null for "not base64-shaped" and for "decoded fine but isn't a Shopify domain", but the case in between — base64 charset, decodes (or fails to decode), doesn't form a URL — escapes as an exception.
This matters because callers are written against the null contract. In @shopify/shopify-app-react-router, validateShopAndHostParams does:
const host = api.utils.sanitizeHost(url.searchParams.get('host'));
if (!host) {
logger.debug('Invalid host, rendering App Bridge', {shop, host: url.searchParams.get('host')});
throw renderAppBridgeOrError(request, params);
}
That if (!host) branch is exactly the intended handling for an invalid host, and the throw skips it. The result is an unhandled TypeError out of authenticate.admin(), which surfaces as a 500 rather than the App Bridge re-acquisition page.
We hit this in production: an automated scanner requested our embedded entry point with ?host=9995625306531699999 (a digit string that happens to satisfy the base64 character class), and every such request returned 500 and raised our error alerting.
Expected behavior
sanitizeHost(host) returns null for any host it cannot validate, including one that is base64-shaped but does not decode to a URL. throwOnInvalid: true then raises InvalidHostError, consistent with every other invalid input.
Actual behavior
It throws the raw underlying error — TypeError: Invalid URL from the URL constructor, or DOMException: The string to be decoded is not correctly encoded. from atob — regardless of throwOnInvalid.
Steps to reproduce the problem
- Save this as
repro.mjsin a project with@shopify/shopify-apiinstalled:
import '@shopify/shopify-api/adapters/node';
import {shopifyApi, ApiVersion} from '@shopify/shopify-api';
const shopify = shopifyApi({
apiKey: 'key',
apiSecretKey: 'secret',
scopes: ['read_products'],
hostName: 'example.com',
apiVersion: ApiVersion.January26,
isEmbeddedApp: true,
});
for (const host of [
'9995625306531699999', // base64 charset, decodes, not a URL
'abcde', // base64 charset, length % 4 === 1
'not-base64!!', // not base64 charset
'YQ', // decodes to "a" -> valid URL, wrong domain
]) {
try {
console.log(JSON.stringify(host), '->', JSON.stringify(shopify.utils.sanitizeHost(host)));
} catch (error) {
console.log(JSON.stringify(host), '-> THREW', `${error.constructor.name}: ${error.message}`);
}
}
node repro.mjs- Observe that the first two throw while the last two return
null.
Equivalently, against an embedded app built on @shopify/shopify-app-react-router, request any route that calls authenticate.admin() with ?shop=<your-shop>.myshopify.com&host=9995625306531699999 and observe a 500.
Debug logs
"9995625306531699999" -> THREW TypeError: Invalid URL
"abcde" -> THREW DOMException: The string to be decoded is not correctly encoded.
"not-base64!!" -> null
"YQ" -> null
Suggested fix
Wrap the decode-and-parse in a try/catch and fall through to the existing null path, so all three invalid shapes are handled identically:
if (sanitizedHost) {
let hostname: string | undefined;
try {
({hostname} = new URL(`https://${decodeHost(sanitizedHost)}`));
} catch {
return throwOnInvalid
? (() => { throw new InvalidHostError('Received invalid host argument'); })()
: null;
}
...
}
Happy to open a PR with a test covering the three shapes if that would help.
- Dominant language
- TypeScript
- Stars
- 540
- Forks
- 225
- Avg merge
- 3d 7h
- Merged PRs (30d)
- 6
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Shopify/shopify-app-js
-
devtools-gardener
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Shopify/shopify-app-js#3309 · 2 comments ·
-
devtools-gardener Stale
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
Shopify/shopify-app-js#3207 · 1 comment ·
-
[Feature] Add Unix socket connection support to PostgreSQL session storage (e.g. Google Cloud SQL) OpenStale
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Shopify/shopify-app-js#3113 · 1 comment ·
-
Stale
Difficulty 1/5 1-3 hours Newbie friendliness 78/100
Shopify/shopify-app-js#3110 · 7 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Shopify/shopify-app-js#3080 · 3 comments ·
All issues in Shopify/shopify-app-js
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100