This document describes what react-native-secure-webview protects against, how,
and — just as importantly — what it does not protect against.
The library is built for flows where you know exactly which web origins are legitimate (your checkout, your identity provider, a 3-D Secure ACS) and want the WebView to refuse everything else. Concretely:
- Top-level navigation control. Every top-level navigation — link taps,
locationchanges, HTTP redirects, history navigations — is validated natively against theallowedOriginsallowlist before it loads. Redirects never reach JS, which is why enforcement cannot live in the JS layer. - Deep-link interception. Custom-scheme URLs (OAuth/3DS return URLs like
myapp://…) are never handed to the WebView or the OS. If the scheme is allow-listed, the URL is reported viaonDeepLink; otherwise it is blocked with a violation. Sub-frames can never trigger deep links. - Minimal bridge surface. The only Web→RN channel is
window.ReactNativeSecureWebView.postMessage(string). Messages are accepted only from the main frame, only from allow-listed origins, and only if the payload is a string. There is no RN→Web channel and no JS injection.- iOS: a fixed
WKUserScriptshim posts to aWKScriptMessageHandler, which validates the sender frame (isMainFrame) and canonicalizes and checks the sender'sWKSecurityOriginagainst the allowlist. - Android: the bridge object is injected via
WebViewCompat.addWebMessageListenerwith the canonical allowlist as origin rules — the object does not exist in non-allow-listed or sub-frame contexts (unlikeaddJavascriptInterface, which is injected into every frame and is deliberately not used). Each message'ssourceOriginandisMainFrameare re-validated on arrival (defense in depth). If the system WebView predatesWEB_MESSAGE_LISTENERsupport, no bridge is installed at all andonErrorfires once withmessage_bridge_not_supported.
- iOS: a fixed
- Fail closed. Invalid allowlist entries are dropped (never widened),
unparseable URLs are blocked, and unsupported features produce structured
errors instead of silently degrading (see Android
ephemeralbelow).
The reference implementation is src/policy.ts (extensively unit-tested in
src/__tests__/policy.test.ts); iOS (ios/SecureWebViewPolicy.mm) and Android
(android/src/main/java/com/securewebview/UrlPolicy.kt) are deliberately small
mirrors of the same rules:
- URLs are parsed, never substring-matched.
https://checkout.example.com.evil.comandhttps://evil.com/checkout.example.comdo not matchhttps://checkout.example.com. - Origins are canonicalized to
scheme://host:portwith lowercase scheme/host and an explicit effective port (443/80when omitted). Matching is exact string equality. No wildcard or subdomain matching exists. - URLs containing credentials (
user@host), whitespace, control characters, backslashes, or non-ASCII bytes are rejected asinvalid_url. IDN hosts must be provided in punycode form. - Hosts with empty labels or trailing dots (
example.com.) are rejected —https://example.com.would otherwise be a same-site cookie / allowlist bypass ambiguity. allowedSchemesrefuseshttp,https,javascript,data,blob,file,filesystem,about,ws,wss,content, andintenteven if explicitly listed.
Both platforms:
- Mixed content disabled (
MIXED_CONTENT_NEVER_ALLOWon Android; WebKit default on iOS). - Popups /
window.openblocked (setSupportMultipleWindows(false);WKUIDelegatereturnsnilfor new-window requests, and navigations without a target frame are cancelled). - No file, content, or filesystem access (
allowFileAccess = false,allowContentAccess = false; thefile:scheme is unnavigable by policy). - Sub-frames (e.g. 3-D Secure ACS iframes) may load http(s) content, but any non-http(s) scheme inside a sub-frame is blocked and can never surface as a deep link.
- No geolocation, no media-capture grants, no downloads.
- Renderer/content-process death is contained: Android returns
truefromonRenderProcessGone(the default would kill the entire app process), tears the dead WebView down, and emitsandroid_render_process_gone; iOS emitsios_content_process_terminatedfromwebViewWebContentProcessDidTerminate. In both cases session state in memory is gone; callreload()to recover.
- iOS:
session="ephemeral"usesWKWebsiteDataStore.nonPersistentDataStore— cookies/storage are scoped to the web view instance and discarded with it. - Android: the cookie store (
CookieManager) is process-wide. There is no honest way to give one WebView an isolated session without either lying (cookies actually persist) or destructively clearing cookies shared with every other WebView in your app.session="ephemeral"therefore fails closed on Android with error codeephemeral_not_supported. If your flow requires true ephemerality on Android, use Custom Tabs in incognito mode or an OS-level auth session instead.
Be explicit with yourself about the threat model. This library does not defend against:
- Compromised or malicious allow-listed origins. If
https://checkout.example.comitself serves hostile JavaScript (XSS, supply chain compromise), that code runs with full access to the page, the session, and thepostMessagebridge. The allowlist bounds where the WebView can navigate, not what allow-listed pages do. - Content inside allow-listed pages. Sub-resources (scripts, images, XHR, iframes) of an allowed page are not filtered beyond platform defaults. An allowed page can embed an iframe from any https origin — required for 3DS — and that iframe's content is not policy-checked.
- A compromised device or app process. Root/jailbreak, hooking frameworks, or malicious code inside your own app can read anything the WebView can.
- TLS interception the OS accepts. Certificate validation is delegated to the platform. No certificate pinning is performed in v0.1.
- Phishing within allowed origins. Path/query are intentionally not matched; any page on an allowed origin can be navigated to.
- Message authenticity beyond origin.
onMessageguarantees the string came from the main frame of an allow-listed origin — nothing more. Treat the payload as untrusted input: validate and parse it defensively. - Data at rest. Persistent sessions use the platform's default cookie/storage stores with the platform's default protections.
Please report anything exploitable privately via GitHub security advisories (or contact the maintainers directly) rather than filing a public issue. You should receive a response within a few days; please allow a fix to be released before public disclosure.