> ## 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, and production hardening.

This guide covers the security model of GBGBridge, including transport safety, content policies, and best practices for production deployments.

## Security Model Overview

GBGBridge operates within the Android WebView security model. The bridge is a thin messaging layer — it does not bypass any platform security mechanisms. All communication between the web journey and the native host goes through `addJavascriptInterface` (incoming) and `evaluateJavascript` (outgoing).

### Trust Boundaries

```mermaid theme={null}
flowchart TB
    subgraph sandbox["Android App Sandbox"]
        native["Native Host Code\n(fully trusted)"]
        bridge["Bridge"]
        webview["WebView\n(sandboxed web)"]
        native <--> bridge
        bridge <--> webview
    end
```

* **Native host code** runs with full app permissions. Handlers can access any Android API.
* **Web content in the WebView** runs in a sandboxed renderer process. It cannot access native APIs except through the bridge interface.
* **The bridge** is the controlled interface between these two zones.

## Transport Security

Bridge messages never traverse the network — they ride on the WebView's JavaScript interface mechanism — but the journey URL itself does, so transport security has two distinct concerns: how messages move between native and web (covered first) and how the journey is loaded over HTTPS (covered second).

### Message Channel

Messages travel over the WebView's built-in JavaScript bridge mechanism:

* **Web → Native**: `window.GBGBridge.postMessage()` — backed by `addJavascriptInterface`. Only methods annotated with `@JavascriptInterface` are callable from JavaScript, and the SDK exposes exactly one: a single `postMessage` entry point that accepts a JSON string. Calls arrive on the WebView's render thread and are posted to the main looper before any handler or delegate runs.
* **Native → Web**: `webView.evaluateJavascript()` — executes JavaScript in the WebView's context.

Neither channel traverses the network. Messages are memory-to-memory within the device. Captured images travel through the bridge as base64-encoded strings in message payloads — the SDK holds them in memory only and never writes them to disk.

### Network Security (Journey Loading)

The web journey itself is loaded over the network. To ensure network security:

* Always use HTTPS in production. Android blocks cleartext HTTP by default (API 28+), and the SDK's library manifest does not relax this.
* Do not enable cleartext traffic globally. Never ship `android:usesCleartextTraffic="true"` in a production manifest. If you need local development access, use a [network security configuration](https://developer.android.com/privacy-and-security/security-config) scoped to development hosts:

```xml theme={null}
<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">127.0.0.1</domain>
    </domain-config>
</network-security-config>
```

Reference it from your manifest's `<application>` element via `android:networkSecurityConfig="@xml/network_security_config"`. The [reference app](https://github.com/gbgplc/gbg-go-android-reference) takes this a step further: its main source set ships the locked-down config above (cleartext only to emulator loopback hosts), and the `debug` source set overrides the same file with a permissive variant for physical-device LAN testing — so release builds always ship the locked-down version.

* Pin certificates if your security requirements demand it. Android's network security configuration supports declarative certificate pinning via `<pin-set>` entries.

<Warning>
  Never call `handler.proceed()` in `WebViewClient.onReceivedSslError`. Proceeding past an SSL error disables certificate validation for that connection and is a common cause of app store rejections. The default behavior (cancel the load) is the correct one — if you override `onReceivedSslError` at all, use it for logging only.
</Warning>

### Credential Passthrough

Authentication cookies and headers for the journey URL are entirely host-owned — the SDK does not read, store, or forward credentials. Use `CookieManager` to manage cookies and `webView.loadUrl(url, additionalHeaders)` to attach headers to the initial journey request.

## Content Security

### Bootstrap Script Injection

The bridge bootstrap script is injected in `WebViewClient.onPageStarted` via `evaluateJavascript`, on every main-frame page load. This is best-effort early injection: unlike iOS's `WKUserScript` at-document-start guarantee, a `<head>` script that synchronously calls `window.GBGBridge.receive` could in principle race the injection. Journey pages tolerate this — the bootstrap only installs a no-op placeholder that the journey replaces.

The default bootstrap script is minimal:

```javascript theme={null}
window.GBGBridge = window.GBGBridge || {};
window.GBGBridge.receive = window.GBGBridge.receive || function(){};
```

### Custom Bootstrap Scripts

If you provide a custom `bootstrapScript` in `BridgeConfiguration`, ensure it does not:

* Expose sensitive native data to the web context.
* Define functions that bypass the structured message protocol.
* Include inline secrets, tokens, or API keys.

```kotlin theme={null}
// Good: Minimal configuration
val config = BridgeConfiguration(
  hostVersion = "1.0.0",
  capabilities = mapOf(/* ... */),
  bootstrapScript = """
    window.GBGBridge = window.GBGBridge || {};
    window.GBGBridge.receive = window.GBGBridge.receive || function(){};
    window.GBGBridge.config = { theme: 'dark' };
  """.trimIndent()
)

// Bad: Exposes secrets
val config = BridgeConfiguration(
  hostVersion = "1.0.0",
  capabilities = mapOf(/* ... */),
  bootstrapScript = """
    window.GBGBridge = window.GBGBridge || {};
    window.GBGBridge.apiKey = '$apiKey';  // DO NOT DO THIS
  """.trimIndent()
)
```

<Note>
  If you attach with a custom `BootstrapInjectingWebViewClient` subclass (see below), `BridgeConfiguration.bootstrapScript` is ignored — the subclass owns its script. Pass the script to the subclass constructor instead.
</Note>

### Navigation Filtering

Deciding which URLs the WebView is allowed to load is your app's job — the SDK deliberately does not impose a navigation policy. To restrict navigation, subclass `BootstrapInjectingWebViewClient`, override `shouldOverrideUrlLoading`, and pass your subclass via `attach(webView, client = ...)` so bootstrap injection survives:

```kotlin theme={null}
class JourneyWebViewClient(bootstrapScript: String) :
  BootstrapInjectingWebViewClient(bootstrapScript) {

  override fun shouldOverrideUrlLoading(
    view: WebView,
    request: WebResourceRequest
  ): Boolean {
    val host = request.url.host ?: return true  // block hostless URLs
    val allowed = host == "app.example.com" || host.endsWith(".example.com")
    return !allowed  // returning true cancels the navigation
  }
}

host.attach(webView, client = JourneyWebViewClient(bootstrapScript))
```

This is the Android equivalent of iOS's `WKNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:)` — the primary, navigation-level gate on what content can ever reach the bridge.

### Message-Level Origin Allowlist

On iOS, the bridge message handler is installed for the main frame only, so iframes can never reach it. Android's `addJavascriptInterface` has no per-frame equivalent — the interface is injected into **every frame** in the WebView. In its place, the Android SDK offers an opt-in, message-level origin gate: `BridgeConfiguration.allowedOrigins`.

```kotlin theme={null}
val config = BridgeConfiguration(
  hostVersion = "1.0.0",
  allowedOrigins = listOf("https://app.example.com")
)
```

When `allowedOrigins` is `null` (the default), no enforcement happens and your navigation policy is the only gate. When set, each inbound `postMessage` is checked against the normalized origin of the WebView's current main-frame URL. On rejection the message is dropped, `lastError` is set, and `delegate.onError` fires with a `SecurityException`.

Both your allowlist entries and the live URL are normalized to a `scheme://host[:port]` origin tuple before comparison:

* Schemes are limited to `http` and `https`; anything else (`file:`, `data:`, `javascript:`, …) is rejected.
* Scheme and host are compared case-insensitively, so `HTTPS://APP.example.com/` matches `https://app.example.com/journey/123`.
* Default ports are elided (`https://app.example.com:443` ≡ `https://app.example.com`); ports outside `1..65535` are rejected.
* A single trailing dot on the host is stripped (`app.example.com.` ≡ `app.example.com` — the same DNS origin).
* Hosts containing `%` are rejected (fail closed on percent-encoded host components and IPv6 zone identifiers).
* Raw internationalized (non-ASCII) hostnames are rejected — use the punycode A-label form (`xn--...`). This forces homograph lookalikes to declare their punycode, making them visible in any review of your allowlist.
* Paths, queries, and fragments are ignored; only the origin tuple is compared.

The configuration is validated at construction: `BridgeConfiguration` throws `IllegalArgumentException` if `allowedOrigins` is an empty list (a kill-switch with no opt-out — pass `null` to disable enforcement instead) or contains a malformed entry. Misconfigurations surface as a single construction-time failure rather than every message being silently dropped at runtime.

<Warning>
  `allowedOrigins` is **not a security boundary**. Because `addJavascriptInterface` injects into every frame and the SDK cannot identify which frame called `postMessage`, the check compares against the main-frame URL only — a hostile sub-frame inside an allowlisted page can still drive the bridge. There is also a small window between a message being posted and the origin check running on the main thread, during which a same-session navigation is invisible to the gate. Treat `allowedOrigins` as defense-in-depth against misconfiguration (the wrong URL being loaded into the journey WebView), layered on top of navigation filtering via `shouldOverrideUrlLoading` — never as a substitute for it.
</Warning>

## Message Validation

### Incoming Message Decoding

`BridgeHost` decodes incoming messages using `kotlinx.serialization`. If a message doesn't conform to the `BridgeMessage` structure, it is rejected, `lastError` is set, and `delegate.onError` is invoked with the decoding failure. Malformed messages never reach handlers.

### Action Validation

Handlers are routed by exact string match on the `action` field. Register handlers only for actions you expect. Unexpected actions go to `pendingRequests` where you can inspect and respond to them.

### Data Validation in Handlers

Always validate the `data` payload in your handlers. The web content is a less-trusted zone — treat incoming data the same way you would treat user input.

```kotlin theme={null}
override fun handle(request: BridgeMessage, responder: BridgeResponder) {
  // Validate required fields
  val side = (request.payload.data?.get("side") as? JsonPrimitive)?.contentOrNull
  if (side !in listOf("front", "back")) {
    responder.respond(
      status = BridgeResponseStatus.ERROR,
      error = BridgeErrorPayload(
        code = "INVALID_PARAMS",
        message = "Missing or invalid 'side' parameter. Expected 'front' or 'back'.",
        recoverable = true
      )
    )
    return
  }

  // Proceed with validated data
  performCapture(side, responder)
}
```

## Permission Management

### Native Permissions

GBGBridge itself does not request any Android permissions. Your capability handlers are responsible for checking and requesting permissions as needed.

<Tip>
  Check permission status before performing the operation, and return a clear error if permission is denied.
</Tip>

```kotlin theme={null}
class DocumentCaptureHandler(
  private val context: Context
) : BridgeCapabilityHandler {

  override val action = "camera.document.capture"

  override fun handle(request: BridgeMessage, responder: BridgeResponder) {
    val granted = ContextCompat.checkSelfPermission(
      context, Manifest.permission.CAMERA
    ) == PackageManager.PERMISSION_GRANTED

    if (!granted) {
      responder.respond(
        status = BridgeResponseStatus.ERROR,
        error = BridgeErrorPayload(
          code = "CAMERA_DENIED",
          message = "Camera access is not available. Please enable it in Settings.",
          recoverable = true
        )
      )
      return
    }

    performCapture(responder)
  }
}
```

Because `handle` is synchronous, you cannot show the runtime permission dialog inline. To request permission as part of the capture flow, retain the responder, launch the request from your UI (for example with `ActivityResultContracts.RequestPermission`), and call `responder.respond(...)` on the main thread once the result arrives. The SDK's `CameraDetector.check(context)` gives you a quick snapshot of camera hardware availability and permission state — note that it reports only `GRANTED` or `NOT_DETERMINED`, because Android cannot distinguish "never asked" from "permanently denied" without app-side state; set the richer `DENIED`/`RESTRICTED` states on your capture capability after running your own permission flow.

### Manifest Requirements

Declare every permission your handlers might use. Android's equivalent of a missing iOS usage description is quieter but just as fatal: requesting a runtime permission that is not declared in the manifest is automatically denied without ever showing the user a dialog.

| Capability      | Manifest entry                                                   |
| --------------- | ---------------------------------------------------------------- |
| Journey loading | `<uses-permission android:name="android.permission.INTERNET" />` |
| Camera capture  | `<uses-permission android:name="android.permission.CAMERA" />`   |

If you host camera capture, also declare the hardware feature as optional so devices without a camera can still install your app:

```xml theme={null}
<uses-feature android:name="android.hardware.camera" android:required="false" />
```

The system photo picker (`ActivityResultContracts.PickVisualMedia`) requires no permission at all, which makes it a low-friction capture fallback.

## Production Checklist

Before shipping your integration:

* [ ] **HTTPS only** — Journey URL uses HTTPS. No unscoped cleartext exceptions; debug-only network security config overrides live in the `debug` source set.
* [ ] **No secrets in bootstrap scripts** — Custom bootstrap scripts contain no API keys or tokens.
* [ ] **Input validation** — All handlers validate incoming `data` payloads.
* [ ] **Permission handling** — Handlers check and request permissions before accessing protected resources.
* [ ] **Manifest entries** — All permissions handlers might request are declared.
* [ ] **Error information** — Error responses do not leak internal implementation details.
* [ ] **Navigation policy** — `shouldOverrideUrlLoading` restricts the WebView to trusted origins; `allowedOrigins` considered as a defense-in-depth layer.
* [ ] **SSL errors fail closed** — No `handler.proceed()` in `onReceivedSslError`.
* [ ] **Debugging disabled in release** — `WebView.setWebContentsDebuggingEnabled(true)` is gated on `FLAG_DEBUGGABLE` (or debug builds only).
* [ ] **Certificate pinning** — Evaluated whether certificate pinning is required for your security posture.

## Next Steps

* [Troubleshooting Guide](/docs/go-v2/developer-integration/sdks/android/troubleshooting) — Diagnosing security-related issues
* [Embedding Guide](/docs/go-v2/developer-integration/sdks/android/embedding) — Secure WebView configuration
* [API Reference](/docs/go-v2/developer-integration/sdks/android/api-reference) — Configuration and setup API
