@shopify/shopify-app-remix 6.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/cjs/server/authenticate/admin/authenticate.js +6 -1
  2. package/dist/cjs/server/authenticate/admin/authenticate.js.map +1 -1
  3. package/dist/cjs/server/authenticate/admin/helpers/validate-redirect-url.js +49 -0
  4. package/dist/cjs/server/authenticate/admin/helpers/validate-redirect-url.js.map +1 -1
  5. package/dist/cjs/server/version.js +1 -1
  6. package/dist/cjs/server/version.js.map +1 -1
  7. package/dist/esm/server/authenticate/admin/authenticate.mjs +6 -1
  8. package/dist/esm/server/authenticate/admin/authenticate.mjs.map +1 -1
  9. package/dist/esm/server/authenticate/admin/helpers/validate-redirect-url.mjs +49 -1
  10. package/dist/esm/server/authenticate/admin/helpers/validate-redirect-url.mjs.map +1 -1
  11. package/dist/esm/server/version.mjs +1 -1
  12. package/dist/esm/server/version.mjs.map +1 -1
  13. package/dist/ts/server/authenticate/admin/authenticate.d.ts.map +1 -1
  14. package/dist/ts/server/authenticate/admin/helpers/index.d.ts +1 -0
  15. package/dist/ts/server/authenticate/admin/helpers/index.d.ts.map +1 -1
  16. package/dist/ts/server/authenticate/admin/helpers/validate-redirect-url.d.ts +16 -0
  17. package/dist/ts/server/authenticate/admin/helpers/validate-redirect-url.d.ts.map +1 -1
  18. package/dist/ts/server/version.d.ts +1 -1
  19. package/dist/tsconfig.build.tsbuildinfo +1 -1
  20. package/package.json +6 -6
  21. package/src/server/authenticate/admin/authenticate.ts +13 -2
  22. package/src/server/authenticate/admin/helpers/index.ts +1 -0
  23. package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +59 -0
  24. package/src/server/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopify/shopify-app-remix",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "description": "Shopify Remix - to simplify the building of Shopify Apps with Remix",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -73,16 +73,16 @@
73
73
  "@shopify/generate-docs": "^1.2.3",
74
74
  "@shopify/polaris": "^12.18.0",
75
75
  "@shopify/react-testing": "^6.0.0",
76
- "@shopify/shopify-app-session-storage-memory": "^8.0.0",
76
+ "@shopify/shopify-app-session-storage-memory": "^8.0.1",
77
77
  "@types/react": "^19.2.17",
78
78
  "jest-fetch-mock": "^3.2.0"
79
79
  },
80
80
  "dependencies": {
81
81
  "@remix-run/server-runtime": "^2.17.5",
82
- "@shopify/admin-api-client": "^2.0.0",
83
- "@shopify/shopify-api": "^15.0.0",
84
- "@shopify/shopify-app-session-storage": "^7.0.0",
85
- "@shopify/storefront-api-client": "^2.0.0",
82
+ "@shopify/admin-api-client": "^2.0.1",
83
+ "@shopify/shopify-api": "^15.0.1",
84
+ "@shopify/shopify-app-session-storage": "^7.0.1",
85
+ "@shopify/storefront-api-client": "^2.0.1",
86
86
  "compare-versions": "^6.1.1",
87
87
  "isbot": "^5.2.0"
88
88
  },
@@ -1,4 +1,4 @@
1
- import {JwtPayload, Session} from '@shopify/shopify-api';
1
+ import {JwtPayload, Session, ShopifyError} from '@shopify/shopify-api';
2
2
 
3
3
  import type {BasicParams} from '../../types';
4
4
  import type {AppConfigArg} from '../../config-types';
@@ -30,6 +30,7 @@ import {
30
30
  createAdminApiContext,
31
31
  ensureAppIsEmbeddedIfRequired,
32
32
  ensureSessionTokenSearchParamIfRequired,
33
+ isTrustedExitIframeDestination,
33
34
  redirectFactory,
34
35
  renderAppBridge,
35
36
  validateShopAndHostParams,
@@ -69,7 +70,17 @@ export function authStrategyFactory<ConfigArg extends AppConfigArg>({
69
70
  const url = new URL(request.url);
70
71
 
71
72
  if (url.pathname === config.auth.exitIframePath) {
72
- const destination = url.searchParams.get('exitIframe')!;
73
+ const destination = url.searchParams.get('exitIframe');
74
+
75
+ if (!isTrustedExitIframeDestination(config.appUrl, destination)) {
76
+ logger.warning(
77
+ 'Rejected exit-iframe redirect to an untrusted destination',
78
+ {shop: getShopFromRequest(request), destination},
79
+ );
80
+ throw new ShopifyError(
81
+ 'Invalid exit-iframe destination. Refusing to redirect.',
82
+ );
83
+ }
73
84
 
74
85
  logger.debug('Rendering exit iframe page', {
75
86
  shop: getShopFromRequest(request),
@@ -12,3 +12,4 @@ export * from './redirect';
12
12
  export * from './render-app-bridge';
13
13
  export * from './trigger-after-auth-hook';
14
14
  export * from './validate-shop-and-host-params';
15
+ export * from './validate-redirect-url';
@@ -65,3 +65,62 @@ export function sanitizeRedirectUrl<OptionsArg extends Options>(
65
65
  throw new ShopifyError('Invalid URL. Refusing to redirect');
66
66
  }
67
67
  }
68
+
69
+ const SHOPIFY_ADMIN_HOST = 'admin.shopify.com';
70
+ const SHOP_ADMIN_HOST_REGEX = /\.myshopify\.com$/i;
71
+
72
+ /**
73
+ * Determines whether an exit-iframe destination is one we trust enough to
74
+ * redirect to.
75
+ *
76
+ * The destination comes straight from the request URL, so it is limited to:
77
+ *
78
+ * - the app's own origin, including relative paths (used by the OAuth flow), or
79
+ * - the Shopify admin: `admin.shopify.com`, or `<shop>.myshopify.com` under
80
+ * `/admin` (where billing confirmation URLs live).
81
+ *
82
+ * Other Shopify-owned hosts are intentionally excluded, including storefronts
83
+ * (`<shop>.myshopify.com/`), the CDN (`cdn.shopify.com`) and the community
84
+ * forums (`community.shopify.com`), because their content is not controlled by
85
+ * the app or the admin. Anything not explicitly allowed is refused.
86
+ */
87
+ export function isTrustedExitIframeDestination(
88
+ appUrl: string,
89
+ destination: unknown,
90
+ ): destination is string {
91
+ if (typeof destination !== 'string' || destination.trim() === '') {
92
+ return false;
93
+ }
94
+
95
+ let destinationUrl: URL;
96
+ let appOrigin: string;
97
+ try {
98
+ destinationUrl = new URL(destination, appUrl);
99
+ appOrigin = new URL(appUrl).origin;
100
+ } catch (_error) {
101
+ return false;
102
+ }
103
+
104
+ // The app's own origin (including relative paths) is always allowed.
105
+ if (destinationUrl.origin === appOrigin) {
106
+ return true;
107
+ }
108
+
109
+ // Every other destination must be a Shopify admin URL served over https with
110
+ // no custom port.
111
+ if (destinationUrl.protocol !== 'https:' || destinationUrl.port !== '') {
112
+ return false;
113
+ }
114
+
115
+ const {hostname, pathname} = destinationUrl;
116
+
117
+ if (hostname === SHOPIFY_ADMIN_HOST) {
118
+ return true;
119
+ }
120
+
121
+ return SHOP_ADMIN_HOST_REGEX.test(hostname) && isAdminPath(pathname);
122
+ }
123
+
124
+ function isAdminPath(pathname: string): boolean {
125
+ return pathname === '/admin' || pathname.startsWith('/admin/');
126
+ }
@@ -1 +1 @@
1
- export const SHOPIFY_REMIX_LIBRARY_VERSION = '6.0.0';
1
+ export const SHOPIFY_REMIX_LIBRARY_VERSION = '6.0.2';