diff --git a/platforms/android/README.md b/platforms/android/README.md index 30c4d85c5..1f9c1f46c 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -25,6 +25,7 @@ - [Preload checkout](#preload-checkout) - [Configure checkout](#configure-checkout) - [Color schemes](#color-schemes) + - [Incoming message origin validation](#incoming-message-origin-validation) - [Title localization](#title-localization) - [Current configuration](#current-configuration) - [Checkout lifecycle](#checkout-lifecycle) @@ -258,8 +259,8 @@ ShopifyCheckoutKit.configure { | `sheet` | `CheckoutSheetOptions()` | Customize native sheet presentation such as snap points, dismissal behavior, corner radius, title alignment, toolbar elevation, close icon styling, and the optional drag handle. | | `logLevel` | `LogLevel.WARN` | SDK logging verbosity. Use `LogLevel.DEBUG` during integration. | | `preloading` | `Preloading(enabled = true)` | Enables best-effort checkout preloading before presentation. | -| `allowedMessageOrigins` | `emptySet()` | Extra origins allowed to send checkout protocol messages. | -| `onMessageRejected` | `null` | Observes messages rejected by origin validation. | +| `allowedMessageOrigins` | `emptySet()` | Origins trusted to send incoming checkout messages. Empty trusts every origin (open by default). See [Incoming message origin validation](#incoming-message-origin-validation). | +| `onMessageRejected` | `null` | Callback invoked when a message is dropped by origin validation. Defaults to logging at debug level. | ### Color schemes @@ -348,6 +349,48 @@ Set `dragHandle.visible = true` to show a fixed, visual-only drag handle at the when `dismissal.dragToDismissEnabled = false` so disabled drag gestures are not presented as available. Configure `dragHandleColor` in `ColorScheme` to override the default header-font-derived handle color. +### Incoming message origin validation + +The native WebView is a private, app-controlled runtime, so Checkout Kit is +**open by default**: with an empty `allowedMessageOrigins`, incoming +checkout-protocol messages from any origin are accepted. Provide one or more +origins to restrict which origins are trusted; the loaded checkout origin and +`shop.app` (including its subdomains) are always trusted as well. + +```kotlin +ShopifyCheckoutKit.configure { + it.allowedMessageOrigins = setOf( + "https://checkout.example.com", + "https://*.example.com", + ) +} +``` + +Each entry may be an exact origin (`https://example.com`), a wildcard subdomain +(`https://*.example.com`, matching subdomains but not the apex), or `"*"` to +explicitly trust every origin. + +Exact and wildcard entries accept an optional trailing slash. Exact entries +must not include credentials, paths, queries, or fragments. For example, +`https://example.com/` is accepted, while `https://user@example.com` and +`https://example.com/path` are ignored. + +Messages dropped by origin validation are logged at debug level. To observe +them instead, set `onMessageRejected`: + +```kotlin +ShopifyCheckoutKit.configure { + it.onMessageRejected = { rejected -> + Log.w("Checkout", "Dropped ${rejected.origin}: ${rejected.reason}") + } +} +``` + +> [!WARNING] +> The `RejectedMessage` payload is untrusted — it was dropped precisely because +> its origin was not in the allowlist. Incoming messages are advisory and are +> never treated as an authoritative source of checkout state. + ### Title localization Override `checkout_web_view_title` in your app resources: @@ -364,30 +407,6 @@ Override `checkout_web_view_title` in your app resources: val configuration = ShopifyCheckoutKit.getConfiguration() ``` -### Incoming message origin validation - -Native checkout accepts messages from every origin by default. To restrict messages, configure one -or more exact origins or wildcard subdomains. The checkout URL's origin and `shop.app` remain -trusted automatically. - -```kotlin -ShopifyCheckoutKit.configure { - it.allowedMessageOrigins = setOf( - "https://checkout.example.com", - "https://*.example.org", - ) - it.onMessageRejected = { rejection -> - reportRejectedOrigin(rejection.origin, rejection.reason) - } -} -``` - -Exact entries accept an optional trailing slash, but not credentials, paths, queries, or fragments. -For example, `https://checkout.example.com/` is accepted, while -`https://user@checkout.example.com` and `https://checkout.example.com/path` are ignored. Wildcard -entries require the scheme and match subdomains only; `https://*.example.org` does not match -`https://example.org`. Use `"*"` to explicitly disable origin validation. - ## Checkout lifecycle Use `onFail` and `onDismiss` for checkout outcomes handled by your app. Use `CheckoutProtocol.Client` for typed checkout state, including completion. These descriptors wrap checkout protocol messages defined in the [protocol schema](../../protocol/services/shopping/embedded.openrpc.json). diff --git a/platforms/react-native/README.md b/platforms/react-native/README.md index 166e0a803..97e9ad5ba 100644 --- a/platforms/react-native/README.md +++ b/platforms/react-native/README.md @@ -37,6 +37,7 @@ experiences. - [Usage with the Shopify Storefront API](#usage-with-the-shopify-storefront-api) - [Configuration](#configuration) - [Colors](#colors) + - [Incoming message origin validation](#incoming-message-origin-validation) - [Localization](#localization) - [Checkout Sheet title](#checkout-sheet-title) - [iOS - Localization](#ios---localization) @@ -345,6 +346,7 @@ instance of the `ShopifyCheckout` class. | `preloading` | | `true` | Enable/disable [preloading](#preloading). | | `colors` | | `{}` | An object with `ios` and `android` properties to override the colors for iOS and Android platforms individually. See [`colors`](#colors) for more information. | | `logLevel` | | `error` | Sets the log level for the native SDK. Use `LogLevel.debug` for verbose logging during development, or `LogLevel.error` for production. | +| `allowedMessageOrigins` | | `[]` | Origins trusted to send incoming checkout messages. Empty trusts every origin (open by default). See [Incoming message origin validation](#incoming-message-origin-validation). | Here's an example of how a fully customized configuration object might look: @@ -387,6 +389,45 @@ function AppWithContext() { const shopifyCheckout = new ShopifyCheckout(config); ``` +### Incoming message origin validation + +Checkout Kit runs on the native iOS and Android web views, which are private, +app-controlled runtimes. It is therefore **open by default**: with an empty +`allowedMessageOrigins`, incoming checkout-protocol messages from any origin are +accepted. Provide one or more origins to restrict which origins are trusted; the +loaded checkout origin and `shop.app` (including its subdomains) are always +trusted as well. + +```tsx +const config: Configuration = { + allowedMessageOrigins: [ + 'https://checkout.example.com', + 'https://*.example.com', + ], +}; +``` + +Each entry may be an exact origin (`https://example.com`), a wildcard subdomain +(`https://*.example.com`, matching subdomains but not the apex), or `'*'` to +explicitly trust every origin. Exact and wildcard entries accept an optional +trailing slash. Exact entries must not include credentials, paths, queries, or +fragments. Messages dropped by origin validation are logged by the native SDK +at debug level. To observe them instead, configure `onMessageRejected`: + +```tsx +const config: Configuration = { + allowedMessageOrigins: ['https://checkout.example.com'], + onMessageRejected: ({origin, message, reason}) => { + console.warn(`Dropped message from ${origin}: ${reason}`, message); + }, +}; +``` + +> [!NOTE] +> Incoming messages are advisory (lifecycle/UI signals) and are never treated as +> an authoritative source of checkout state, so origin validation is defense in +> depth. + ### Colors The SDK defaults to the `automatic` color scheme option, will switches between diff --git a/platforms/web/README.md b/platforms/web/README.md index 849ec075e..66b2ba009 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -36,6 +36,8 @@ Check out our blog to - [`target`](#target) - [`appearance`](#appearance) - [`log-level`](#log-level) + - [`allowed-origins`](#allowed-origins) + - [`onMessageRejected`](#onmessagerejected) - [Popup dimensions](#popup-dimensions) - [Overlay scrim](#overlay-scrim) - [Checkout lifecycle](#checkout-lifecycle) @@ -397,6 +399,59 @@ Use `"debug"` while wiring up `src` and event handlers during integration, or checkout.logLevel = 'debug'; ``` +### `allowed-origins` + +Controls which origins are trusted to post incoming checkout-protocol messages +to the component. On web, checkout is **closed by default**: with no configured +origins, only the cart URL origin (from `src`) and `shop.app` (including its +subdomains) are trusted. Messages from any other origin are dropped. + +Provide extra origins as a space- or comma-separated list. Each entry may be: + +| Entry | Matches | +| ------------------------- | -------------------------------------------------------------- | +| `https://example.com` | That exact origin. | +| `https://*.example.com` | Any subdomain of `example.com` (not the apex `example.com`). | +| `*` | Every origin — disables origin validation entirely. | + +Exact and wildcard entries accept an optional trailing slash. Exact entries +must not include credentials, paths, queries, or fragments. For example, +`https://example.com/` is accepted, while `https://user@example.com` and +`https://example.com/path` are ignored. + +```html + +``` + +```ts +checkout.allowedOrigins = ['https://checkout.example.com', 'https://*.example.com']; +``` + +> [!NOTE] +> Incoming messages are advisory (lifecycle/UI signals) and are never treated +> as an authoritative source of checkout state, so origin validation is defense +> in depth. Use `*` only when you understand the trade-off — in the shared +> browser, other pages and extensions can post to the component. + +Invalid entries are ignored, and a warning is logged at `log-level="warn"` or +more verbose. + +### `onMessageRejected` + +A property-only callback invoked whenever an incoming message is dropped by +origin validation. The smart default logs a warning; assign a function to +observe rejected messages instead (for example, to report them). + +```ts +checkout.onMessageRejected = ({ origin, data, reason }) => { + console.warn(`Dropped message from ${origin}: ${reason}`, data); +}; +``` + +> [!WARNING] +> The payload is untrusted — it was dropped precisely because its origin was +> not in the allowlist. Do not derive checkout state from it. + ### Popup dimensions When `target="popup"`, the popup is centered over the host window. Defaults