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 throughaddJavascriptInterface (incoming) and evaluateJavascript (outgoing).
Trust Boundaries
- 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 byaddJavascriptInterface. Only methods annotated with@JavascriptInterfaceare callable from JavaScript, and the SDK exposes exactly one: a singlepostMessageentry 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.
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 scoped to development hosts:
<application> element via android:networkSecurityConfig="@xml/network_security_config". The reference app 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.
Credential Passthrough
Authentication cookies and headers for the journey URL are entirely host-owned — the SDK does not read, store, or forward credentials. UseCookieManager 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 inWebViewClient.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:
Custom Bootstrap Scripts
If you provide a custombootstrapScript 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.
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.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, subclassBootstrapInjectingWebViewClient, override shouldOverrideUrlLoading, and pass your subclass via attach(webView, client = ...) so bootstrap injection survives:
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’saddJavascriptInterface 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.
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
httpandhttps; anything else (file:,data:,javascript:, …) is rejected. - Scheme and host are compared case-insensitively, so
HTTPS://APP.example.com/matcheshttps://app.example.com/journey/123. - Default ports are elided (
https://app.example.com:443≡https://app.example.com); ports outside1..65535are 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.
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.
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 theaction 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 thedata payload in your handlers. The web content is a less-trusted zone — treat incoming data the same way you would treat user input.
Permission Management
Native Permissions
GBGBridge itself does not request any Android permissions. Your capability handlers are responsible for checking and requesting permissions as needed.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" /> |
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
debugsource set. - No secrets in bootstrap scripts — Custom bootstrap scripts contain no API keys or tokens.
- Input validation — All handlers validate incoming
datapayloads. - 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 —
shouldOverrideUrlLoadingrestricts the WebView to trusted origins;allowedOriginsconsidered as a defense-in-depth layer. - SSL errors fail closed — No
handler.proceed()inonReceivedSslError. - Debugging disabled in release —
WebView.setWebContentsDebuggingEnabled(true)is gated onFLAG_DEBUGGABLE(or debug builds only). - Certificate pinning — Evaluated whether certificate pinning is required for your security posture.
Next Steps
- Troubleshooting Guide — Diagnosing security-related issues
- Embedding Guide — Secure WebView configuration
- API Reference — Configuration and setup API