> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Trust boundaries, transport security, CSP, origin allowlist, and production hardening.

This guide covers the security model of the GBGBridge Web SDK, including transport safety, content policies, origin allowlist hardening, token-exchange responsibilities, and production-deployment best practices.

## Security model overview

GBGBridge runs entirely in the browser. The bridge is a structured-message layer on top of the browser's native `postMessage` channel — it does not bypass any browser security mechanism. Three guarantees underpin the security model:

1. **Cross-origin isolation** — The host page and the journey iframe are separate origins. Same-origin policy prevents either from reading the other's DOM, cookies, or storage. The bridge is the only sanctioned channel between them.
2. **Origin allowlist** — The SDK enforces a strict allowlist on both inbound and outbound messages. Messages to or from origins not in the allowlist are silently dropped.
3. **Envelope validation** — Every inbound message is validated against the protocol envelope before reaching any handler. Malformed messages are dropped before they can be routed.

### Trust boundaries

```mermaid theme={null}
flowchart TB
    subgraph host["Host Page (your code, fully trusted)"]
        page["Host page JavaScript"]
        sdk["@gbg/go-bridge-web SDK"]
    end
    subgraph iframe["Journey iframe (cross-origin, less trusted)"]
        journey["Journey JavaScript"]
    end
    backend["Your backend"]

    page <--> sdk
    sdk <-->|postMessage<br/>origin-validated| journey
    page -->|HTTPS<br/>journey URL fetch| backend
    backend -->|tokenized journey URL| page
```

* **Host page code** runs with full access to the host's origin (cookies, storage, network).
* **Journey iframe content** runs in a sandboxed cross-origin context. It cannot access the host page's DOM, storage, or APIs — only the bridge.
* **The bridge** is the controlled interface. Every message crosses an origin boundary and is validated by the SDK before reaching either side.
* **Your backend** does the [token exchange](/docs/go-v2/developer-integration/sdks/web/journey-url) and returns a tokenized journey URL. Credentials never reach the browser.

## Transport security

Bridge messages travel over the browser's in-process `postMessage` channel — they never leave the device. Network security applies separately to the journey URL load and any backend calls.

### Message channel

| Direction     | Mechanism                                                                                                           |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| Host → Iframe | `iframe.contentWindow.postMessage(message, targetOrigin)`                                                           |
| Iframe → Host | `window.postMessage(message, "*")` from inside the iframe, received via the host page's `window` `message` listener |

Both directions stay inside the browser's process. There is no IPC across processes (browser-internal), and no network call between iframe and host.

### Network security (journey URL load)

The journey URL is loaded over the network. Apply standard web transport hygiene:

* **HTTPS in production.** Browsers block mixed content — an `https://` host page loading an `http://` iframe will fail silently. Local development on `http://localhost` works in both directions because browsers treat `localhost` as a secure context.
* **HSTS** on the journey origin if you control it. Forces HTTPS even on first contact.
* **CSP `frame-src` directive** on the host page (covered below).

## Origin allowlist

The `allowedOrigins` array passed to `BridgeHost` / `BridgeHostProvider` is the **load-bearing** security control. The SDK enforces it on three independent checks per inbound message:

1. `event.source !== iframe.contentWindow` → drop silently.
2. `event.origin` not in the normalized allowlist → drop silently.
3. Envelope validation fails → drop silently.

And on outbound:

4. `targetOrigin` resolved from `iframe.src` not in the allowlist → drop with `console.warn`.

This means a script on the host page that repoints `iframe.src` to a hostile origin cannot use the bridge to leak data or invoke host capabilities — the outbound check fails and the inbound source check fails.

<Note>
  Origin allowlist enforcement is **defence in depth**: this is the host-page half of the work shipped under [GGO-13990](https://gbg.atlassian.net/browse/GGO-13990) (US1 + US2). The iframe-side journey enforces the same allowlist against `event.origin` of inbound messages from the host. Both ends of the bridge validate origin independently — neither half can be skipped by a script on the other side.
</Note>

### Format rules

* **Exact origins only.** No paths, no trailing slashes, no wildcards.
* **Match scheme.** `https://example.com` and `http://example.com` are different origins.
* **Default ports omitted.** `https://example.com` and `https://example.com:443` are the same; the SDK normalizes both to the canonical form, but write the canonical form yourself to avoid drift.
* **Non-default ports required.** `https://example.com:8443` requires the explicit port.
* **Empty arrays throw.** `new BridgeHost({ allowedOrigins: [], ... })` raises at construction. There is no implicit "allow all" — this is by design.

```typescript theme={null}
// Good
allowedOrigins: ["https://journey.example.com", "https://staging.journey.example.com"]

// Bad — trailing slash, path component, wildcard
allowedOrigins: ["https://journey.example.com/", "https://journey.example.com/start", "https://*.example.com"]
```

### Multiple environments

If your host serves both production and staging, list both explicit origins. If the journey URL might be on yet a third origin (e.g., a per-tenant subdomain), build the allowlist dynamically:

```typescript theme={null}
const allowedOrigins = [
  new URL(journeyUrl).origin,
  "https://fallback.journey.example.com",
];
```

Avoid building allowlists from user-controlled input — a malicious URL could expand the allowlist past your intended trust set.

### Complete multi-environment example

For a host page that serves both production and staging, three controls work together. All three reference the same set of journey origins:

```tsx theme={null}
// 1. CSP frame-src on the host page response header
// Content-Security-Policy: frame-src https://journey.example.com https://staging.journey.example.com;

// 2. Origin allowlist on the SDK
const JOURNEY_ORIGINS = {
  production: "https://journey.example.com",
  staging: "https://staging.journey.example.com",
} as const;

function JourneyView({ env, journeyUrl }: { env: keyof typeof JOURNEY_ORIGINS; journeyUrl: string }) {
  const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);

  return (
    <BridgeHostProvider
      iframe={iframe}
      allowedOrigins={[JOURNEY_ORIGINS[env]]}
      hostVersion="1.0.0"
      capabilities={{ "camera.document": { supported: true, version: "1.0" } }}
    >
      <DocumentCaptureHandler />
      {/* 3. Iframe allow attribute scoped to camera only */}
      <iframe ref={setIframe} src={journeyUrl} allow="camera" />
    </BridgeHostProvider>
  );
}
```

Three things to keep aligned:

1. **CSP `frame-src`** lists both environments — without it, the browser blocks the iframe load entirely.
2. **`allowedOrigins`** is per-instance, scoped to the single environment in use — narrower than CSP. Hard-coding a tighter allowlist than your CSP is intentional.
3. **iframe `allow`** is environment-agnostic — same permission set for production and staging.

If you switch environments at runtime, force a Provider remount (`key={env}`) so the `allowedOrigins` snapshot rebuilds with the new origin. Don't just change the `allowedOrigins` prop — it's snapshotted at attach time.

## Content Security Policy (CSP)

If your host page sets a CSP header, the journey origin must appear in `frame-src` (or `child-src` as a fallback for older browsers):

```
Content-Security-Policy: frame-src https://journey.example.com;
```

For multiple environments, list each explicitly. Avoid `frame-src *` in production — it disables a meaningful defence-in-depth layer.

If the journey itself loads sub-resources from third-party origins (analytics, fonts), those are constrained by the **journey's** CSP, not yours. Coordinate with the journey team if you see CSP failures in the iframe.

## Iframe permission delegation

Browser permissions (camera, microphone, geolocation, etc.) are not inherited by iframes by default. Use the iframe `allow` attribute to delegate the permissions your journey might need:

```html theme={null}
<iframe src="https://journey.example.com" allow="camera; microphone"></iframe>
```

Even when your host handles capture via the bridge, set the appropriate `allow` values — the journey may invoke `getUserMedia` for a fallback flow (e.g., if `capability.query` returns `supported: false` for `camera.document`).

Use the **least-permissive** set of permissions your integration actually requires. Don't include `geolocation` or `microphone` if the journey doesn't use them.

## No bootstrap script injection

Unlike iOS (`WKUserScript`) and Android (`WebViewClient.onPageStarted` + `addJavascriptInterface`), the Web SDK does **not** inject any script into the journey iframe. Cross-origin restrictions would prevent it anyway — the host page cannot execute JavaScript inside a cross-origin iframe.

The journey iframe loads its own SDK (`@gbgplc-internal/ggo-native-bridge` and its React variant) as part of its bundle. The host and iframe agree on the wire protocol; neither party can modify the other's runtime.

This is a hard browser constraint, not a design choice — but it also means there's no bootstrap configuration to manage, and no risk of accidentally exposing host secrets via injected globals.

## Message validation

Two layers of validation apply to every inbound message: shape validation at the transport layer, and payload validation inside your own handlers. Both matter — one keeps malformed messages out of your handlers, the other keeps malicious payloads from acting on your handler's assumptions.

### Envelope validation

Every inbound message goes through `HostIframeChannel.isValidEnvelope` before reaching `BridgeHost`. The checks:

* `version` is a string
* `correlationId` is a string
* `type` is `"request"`, `"response"`, or `"event"`
* `timestamp` is a number
* `payload` exists and is an object
* `payload.action` is a non-empty string

Failures are dropped silently. Malformed envelopes never reach handlers, the delegate, or `pendingRequests`.

### Action validation

Handlers route by exact string match on `payload.action`. Register handlers only for actions you expect. Unknown actions:

* Go to `pendingRequests` (capped at 50; overflow dropped).
* Fire `delegate.onUnhandledRequest` so you can decide whether to respond manually.

### Data validation in handlers

The iframe is the less-trusted zone. Treat its `payload.data` the same way you would treat any external input — validate every field your handler reads.

```typescript theme={null}
host.registerHandler("camera.document.capture", {
  handle: async (request, responder) => {
    const data = (request.payload as { data: Record<string, unknown> }).data;
    const side = data.side;

    if (side !== "front" && side !== "back") {
      responder.error({
        code: "INVALID_REQUEST",
        message: "Missing or invalid 'side' parameter. Expected 'front' or 'back'.",
        recoverable: true,
      });
      return;
    }

    const image = await captureDocument(side);
    responder.success({ image });
  },
});
```

## Token exchange and credential safety

The journey URL is typically a single-use, signed URL minted by your backend. Three rules:

1. **Backend mints the URL, not the browser.** Use `@gbg/go-core` server-side to authenticate with GBG and obtain the tokenized journey URL. The host page receives only the URL.
2. **Credentials never reach the browser.** `@gbg/go-core`'s OAuth flow uses a client secret. Exposing it in browser code lets anyone mint sessions against your tenant. Always run that flow server-side.
3. **The journey URL is a short-lived token.** It typically expires within minutes and is single-use. Don't cache it in `localStorage` or pass it through unprotected channels.

For the full backend-to-frontend handoff pattern, see [Journey URL](/docs/go-v2/developer-integration/sdks/web/journey-url).

## Browser permission handling

GBGBridge itself does not request any browser permissions. Your capability handlers are responsible for requesting and checking permissions before performing the operation.

```typescript theme={null}
host.registerHandler("camera.document.capture", {
  handle: async (_request, responder) => {
    if (!navigator.permissions) {
      // Fallback: just attempt getUserMedia
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true });
        // ... perform capture ...
        responder.success({ image: "..." });
      } catch {
        responder.error({
          code: "PERMISSION_DENIED",
          message: "Camera access denied",
          recoverable: true,
        });
      }
      return;
    }

    const status = await navigator.permissions.query({ name: "camera" as PermissionName });
    if (status.state === "denied") {
      responder.error({
        code: "PERMISSION_DENIED",
        message: "Camera access denied. Enable it in browser settings.",
        recoverable: true,
      });
      return;
    }

    // Proceed with capture
    const image = await captureDocument();
    responder.success({ image });
  },
});
```

Report the current permission state in your `capabilities` declaration when possible — the journey uses `permissionState` to surface a permission prompt before triggering the request.

```typescript theme={null}
const capabilities = {
  "camera.document": {
    supported: true,
    version: "1.0",
    permissionState: cameraPermissionState, // "granted" | "prompt" | "denied"
  },
};
```

## Production checklist

Before shipping your integration:

* [ ] **HTTPS only.** Journey URL is HTTPS. Host page is HTTPS in production. No mixed content.
* [ ] **Exact origin allowlist.** `allowedOrigins` contains only the journey origins you trust. No wildcards, no paths, no trailing slashes.
* [ ] **CSP `frame-src`.** Host page CSP allows the journey origin(s). No `frame-src *`.
* [ ] **iframe `allow` attribute.** Set the minimum browser permissions the journey needs (typically `camera` for capture flows).
* [ ] **Backend token exchange.** `@gbg/go-core` runs server-side. Client secret never ships in browser code. See [Journey URL](/docs/go-v2/developer-integration/sdks/web/journey-url).
* [ ] **Input validation in handlers.** Every handler validates `payload.data` before acting on it.
* [ ] **No secrets in bridge messages.** Events and responses contain no tokens, API keys, or personally-identifying data the journey doesn't already have.
* [ ] **Error responses sanitized.** Error messages don't leak internal implementation details (stack traces, file paths, internal IDs).
* [ ] **Multiple environments separated.** Staging and production journeys have distinct origins and distinct allowlists.
* [ ] **Detach on teardown.** When the iframe is removed, `host.detach()` (or provider unmount) runs to release the message listener.

## Next steps

* [Journey URL](/docs/go-v2/developer-integration/sdks/web/journey-url) — Backend token exchange and journey URL construction.
* [Integration Checklist](/docs/go-v2/developer-integration/sdks/web/integration-checklist) — Pre-flight checks before shipping.
* [Troubleshooting](/docs/go-v2/developer-integration/sdks/web/troubleshooting) — Diagnosing security-related issues.
* [API Reference](/docs/go-v2/developer-integration/sdks/web/api-reference) — Configuration and setup API.
