WASM Security in WebKit/Safari and Mobile Browser Contexts
Problem
Most WASM browser security guidance is written with V8 (Chrome/Node.js) as the reference runtime. WebKit — the engine powering Safari on macOS, all browsers on iOS by App Store policy, and WKWebView in iOS apps — has a substantially different WASM execution model, a more conservative posture on Spectre mitigations, and hard platform constraints on iOS that change the attack surface in ways V8-focused documentation does not address.
The practical consequences for security engineers are real:
- JIT restrictions on iOS: iOS historically prohibited third-party JIT compilation. The JIT entitlement (and later the hardened runtime + JIT entitlement for macOS) means that WASM optimization tiers available on desktop WebKit may be absent or constrained in WKWebView contexts, altering both performance and the security trade-offs of compiled code.
- App Transport Security applies to WASM module loading: ATS enforcement at the OS level means that WASM modules fetched over plain HTTP in an iOS app context are blocked at the network layer — before CSP ever sees the request. Misconfiguring ATS exceptions silently removes this protection.
- SharedArrayBuffer restrictions in WebKit differ from Chrome’s: WebKit implemented its own Spectre mitigation strategy and was more cautious than V8 in restoring
SharedArrayBufferaccess. The practical effect: WASM threading code that works on Chrome may silently degrade or fail on Safari without the developer noticing during desktop testing. - WKWebView JavaScript-WASM bridge: Native iOS apps using
WKWebViewexpose aWKScriptMessageHandlerbridge. If WASM module instantiation or memory access is mediated through this bridge without input validation, it creates an injection path from native code into WASM execution context and vice versa.
Target systems: iOS and macOS applications embedding WKWebView; Progressive Web Apps running on Safari; React Native applications using Hermes or a web view for WASM execution; any team shipping WASM to a mixed browser population where WebKit is a significant share.
Threat Model
- Adversary 1 — ATS exception exploitation: A developer adds a broad
NSAllowsArbitraryLoadsexception to bypass ATS during development and ships it to production. An attacker on the same network (hotel Wi-Fi, coffee shop) performs a MITM, substituting the.wasmmodule fetched over HTTP with a malicious one. The iOS network stack delivers the substituted module; no CSP hash check is in place because the developer assumed ATS covered integrity. - Adversary 2 — WKWebView bridge injection: A native iOS component passes user-controlled data to a
WKWebViewviaevaluateJavaScriptor aWKScriptMessageHandler. The injected data triggersWebAssembly.instantiateon attacker-supplied bytes — compiled and executed within the webview’s origin with access to DOM storage, cookies, and any exported JavaScript APIs. - Adversary 3 — Streaming compilation integrity bypass: A PWA running on Safari uses
WebAssembly.instantiateStreamingto reduce startup latency. The WASM module is loaded from a CDN without a CSP hash or SRI attribute, and the CDN delivers a subtly backdoored version. WebKit compiles and executes the module before the application has a chance to hash-verify the bytes. - Adversary 4 — Memory pressure amplification: A malicious web page served to a mobile Safari user allocates maximum WASM linear memory (up to the iOS per-process limit, which is significantly lower than desktop). The allocation causes the system to terminate background processes and may trigger a crash of the browser itself — a denial-of-service against the user’s session state.
- Adversary 5 — Hermes WASM shim confusion: A React Native application ships a WASM module intended to run in a WKWebView web context. Due to Hermes’s limited WASM support, the module silently falls back to a JavaScript polyfill that lacks the sandboxing properties the security design assumed. Sensitive operations intended to run in the WASM sandbox now execute in the Hermes JS heap with full access to the RN bridge.
WebKit’s WASM Execution Tiers: BBQ and OMG
WebKit’s JavaScriptCore (JSC) implements WASM compilation in two tiers:
BBQ (Build Bytecode Quickly): The baseline compiler. BBQ translates WASM bytecode to native code quickly with minimal optimization — analogous to Ignition in V8. It is always present and is the only compilation tier available in contexts where JIT is restricted (certain WKWebView configurations, process sandbox levels).
OMG (Optimized Machine code Generator): The optimizing compiler. OMG applies type inference and advanced code transformations to hot WASM functions. It is only available when the process holds the JIT entitlement and the WebKit process model permits JIT memory (a region that is simultaneously writable and executable, or that uses a split W^X design).
On iOS, the JIT entitlement (com.apple.security.cs.allow-jit) is required for OMG to activate. Before iOS 14.5, this entitlement was unavailable to third-party apps. Safari itself holds the entitlement; WKWebView embeds in third-party apps historically ran BBQ-only. Since iOS 14.5, WKWebView gained access to the JIT entitlement under certain conditions, but the exact behavior differs between app configurations and iOS releases.
Security implication: BBQ-only execution is slower but has a smaller JIT attack surface. OMG introduces a larger, more complex code generator that has historically been a source of JIT compiler bugs — type confusion vulnerabilities, incorrect bounds elimination, speculative execution gadgets. If your threat model includes exploitation of JIT compiler vulnerabilities, BBQ-only execution (forced by running in a non-JIT WKWebView context) is a meaningful defense in depth, at the cost of WASM performance.
WebKit’s Spectre Mitigations: More Conservative Than V8
Spectre mitigations in browsers fall into two categories: reducing timer resolution (to make timing attacks harder) and restricting the APIs that provide high-resolution timers.
V8’s approach: restore SharedArrayBuffer (which provides a precise shared-memory timer) once sites opt into Cross-Origin Isolation via Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. This allows threading-capable WASM on any cross-origin-isolated site.
WebKit’s approach has been more conservative:
- WebKit reduced
performance.now()resolution and added jitter, limiting its usefulness as a Spectre timer. - WebKit was slower to restore
SharedArrayBufferaccess. Even after the cross-origin isolation mechanism was standardized, Safari versions for an extended period either did not supportSharedArrayBufferat all or restricted it in ways that broke WASM threading code. - WebKit’s JIT hardening (constant blinding, NOP sled insertion, randomized code layout) is applied at the BBQ tier as well, not only in OMG, reducing the predictability of JIT-generated code for ROP chain construction.
The practical outcome for WASM threading: teams that test WASM threading only on Chrome, then deploy to Safari, frequently discover that SharedArrayBuffer is unavailable and their threading-dependent WASM module falls back to single-threaded execution — or fails entirely. The security-relevant version of this discovery is the reverse: a developer assumes WebKit’s restriction prevents SharedArrayBuffer use, then is surprised when a newer Safari version exposes it under cross-origin isolation, widening the Spectre surface unexpectedly.
Hardening guidance: Do not rely on WebKit’s conservative SharedArrayBuffer defaults as a security control. Enforce cross-origin isolation headers explicitly and test on Safari. Assume that any future WebKit release may enable SharedArrayBuffer on your origin if COOP/COEP are present.
iOS-Specific WASM Security: App Transport Security
App Transport Security (ATS) is an iOS/macOS OS-level policy that requires HTTPS connections for network requests made by apps in certain contexts. ATS applies to WASM module fetches made from within a WKWebView embedded in a native app — the request passes through the app’s URL session, which enforces ATS before handing the response to WebKit.
ATS provides a transport-layer guarantee: the WASM bytes were fetched over TLS with a valid certificate chain. It does not provide an integrity guarantee over the module contents — a WASM file served over HTTPS from a compromised origin still arrives without error.
Common ATS misconfigurations that undermine this:
<!-- Info.plist — dangerous: disables ATS entirely -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<!-- Info.plist — scoped exception for a CDN domain: still dangerous -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>cdn.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
Both configurations allow WASM modules to be fetched over unencrypted HTTP, making them trivially substitutable by a network-position attacker.
Hardening: Never ship NSAllowsArbitraryLoads: true in production. Audit all NSExceptionDomains entries. If a WASM module must be loaded from a third-party CDN, ensure that domain is not in any ATS exception list. Combine ATS (transport) with SRI or CSP hash verification (integrity) — neither alone is sufficient.
iOS Memory Limits and WASM Module Size
iOS enforces strict per-process memory limits that vary by device model and iOS version. On low-end devices, available memory for a WKWebView process may be 150–300 MB total — not just for WASM linear memory, but for the entire web content process including DOM, JavaScript heap, and compiled WASM code.
WASM linear memory is allocated as a contiguous virtual address region. The WASM spec allows modules to request up to 4 GB of address space (with memory64 extending this further). On desktop Chrome, it is common to see WASM modules allocate 256 MB or more. On iOS, allocations at this scale will trigger the system’s memory pressure daemon (jetsam), which will terminate the web content process — indistinguishable from a browser crash from the user’s perspective.
Security implications of memory pressure:
- Denial of service: A malicious page can allocate maximum WASM memory to force a jetsam kill of the browser process, clearing session state and potentially interrupting security-sensitive operations (payment flows, authentication).
- Memory pressure side channels: Memory allocation timing differences across devices can leak information about available system memory — a weak but real information disclosure.
- OOM-triggered code paths: WASM allocations that fail on iOS but succeed on desktop may trigger error-handling code paths that were not security-reviewed because they never triggered in testing. Allocation failure in WASM is returned as a JavaScript exception; if the application does not handle
WebAssembly.RuntimeErrorfrom OOM, it may leave the application in a partially initialized state.
Hardening:
// Validate memory request before instantiation
const MAX_MOBILE_WASM_PAGES = 256; // 16 MB — conservative for mobile
const requestedPages = wasmMemoryDescriptor.initial;
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) && requestedPages > MAX_MOBILE_WASM_PAGES) {
throw new Error(`WASM module requests ${requestedPages * 64}KB; exceeds mobile memory budget`);
}
WKWebView JavaScript-WASM Bridge Security
WKWebView exposes two primary bridges from native Objective-C/Swift code into the web content process:
evaluateJavaScript(_:completionHandler:)— executes arbitrary JavaScript string in the webview context.WKScriptMessageHandler— a structured message channel from JavaScript to native code; native responses come back viaevaluateJavaScript.
Either bridge becomes dangerous when user-controlled or attacker-controlled data flows through it unvalidated.
Injection via evaluateJavaScript:
// DANGEROUS: user-controlled data injected into JS string
let userInput = getUserInput() // attacker controls this
webView.evaluateJavaScript("loadWasmModule('\(userInput)')")
// If loadWasmModule calls WebAssembly.instantiate on the argument,
// this is a WASM compilation injection primitive.
The safe pattern:
// SAFE: pass data through WKScriptMessageHandler, not string interpolation
// In WKUserContentController, post messages as structured objects.
// In JavaScript, receive via window.addEventListener('message', ...)
// with origin validation, not via eval or dynamic WASM instantiation.
Content Security Policy for WASM in WKWebView:
CSP headers can be injected into WKWebView responses using WKUserScript or by configuring the local server that serves content. The wasm-unsafe-eval CSP directive (distinct from unsafe-eval) allows WASM compilation without allowing eval():
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https://cdn.example.com
WebKit added support for wasm-unsafe-eval in Safari 16. If you must support Safari 15 and earlier, the fallback is 'unsafe-eval' — which also allows JavaScript eval(). This is a meaningful security regression; prefer requiring Safari 16+ for WASM deployments that rely on CSP as a defense.
Trusted Types and WASM Instantiation
The Trusted Types API prevents DOM-injection XSS by requiring that strings assigned to injection sinks (like innerHTML, eval, script creation) pass through a policy object. It does not natively cover WebAssembly.compile and WebAssembly.instantiate, but it can be used to wrap WASM instantiation in a controlled path.
WebKit’s Trusted Types support has lagged Chrome’s — as of early 2026, support is partial. Despite this, establishing a Trusted Types policy that wraps WASM instantiation is valuable for defense in depth on browsers that do support it:
// Create a strict Trusted Types policy
const wasmPolicy = trustedTypes.createPolicy('wasm-loader', {
createScript: (url) => {
// Only allow known WASM module URLs — reject anything else
const allowedModules = [
'https://cdn.example.com/app.wasm',
'/static/worker.wasm',
];
if (!allowedModules.includes(url)) {
throw new TypeError(`Blocked WASM load from disallowed URL: ${url}`);
}
return url;
}
});
// Enforce the policy in your WASM loader
async function loadModule(url) {
const trustedUrl = wasmPolicy.createScript(url); // throws if not in allowlist
const response = await fetch(trustedUrl);
return WebAssembly.instantiateStreaming(response);
}
On browsers without Trusted Types support, this degrades gracefully — trustedTypes is undefined, and you fall back to the allowlist check alone. The allowlist itself is always effective.
WASM Streaming Compilation: Integrity Verification Challenges
WebAssembly.instantiateStreaming compiles a WASM module while the bytes are still being downloaded from the network. This reduces startup latency significantly on mobile networks where download time dominates. It also creates an integrity verification challenge: the module is being compiled before all bytes have arrived, which means a hash of the complete module cannot be computed before compilation begins.
The correct mitigation is not to avoid streaming compilation — the performance cost on mobile is too high. Instead:
-
Serve WASM with SRI from a
<link rel="preload">: Browsers that support SRI on preload will verify the hash before making the response available tofetch(). Streaming compilation then proceeds over the preloaded (and verified) bytes. -
Use a service worker to intercept and verify: A service worker can fetch the WASM module, compute a SHA-256 hash of the response body, compare it against a pinned value, and only then pass the verified
ResponsetoWebAssembly.instantiateStreaming. This works on Safari with service workers (supported since Safari 11.1).
// In service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.endsWith('.wasm')) {
event.respondWith(verifyAndServeWasm(event.request));
}
});
async function verifyAndServeWasm(request) {
const PINNED_HASHES = {
'/static/app.wasm': 'sha256-ABC123...', // base64 SHA-256 of known-good module
};
const response = await fetch(request);
const buffer = await response.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashBase64 = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
const url = new URL(request.url).pathname;
if (PINNED_HASHES[url] && `sha256-${hashBase64}` !== PINNED_HASHES[url]) {
throw new Error(`WASM integrity check failed for ${url}`);
}
return new Response(buffer, { headers: { 'Content-Type': 'application/wasm' } });
}
- Set
Content-Security-Policywith asha256-hash for the WASM source: CSP hash sources work for inline content; for external.wasmfiles the mechanism is SRI, not CSP hashes. Confirm the distinction in your implementation.
React Native and Hermes: WASM Support Limitations
Hermes is Meta’s JavaScript engine used as the default in React Native as of RN 0.70. Hermes does not support WebAssembly. This is a documented limitation — Hermes prioritizes startup performance and bytecode precompilation, and the WASM JIT pipeline conflicts with those goals.
Security implications:
- WASM code silently becomes unreachable: A React Native application that bundles WASM for execution in a Hermes context will find
WebAssemblyis undefined. If the application does not check forWebAssemblyavailability and has a JavaScript fallback, that fallback runs instead — potentially with weaker isolation guarantees. - WKWebView within React Native: Some RN applications embed a
WKWebViewcomponent specifically to run WASM. This webview runs Safari’s JSC, not Hermes — so WASM works, but the security model of the WKWebView (including the JS-native bridge described above) applies. A security design that assumes WASM is isolated in a separate process (the webview process) from Hermes must not pass untrusted data through the bridge. - Third-party WASM polyfills: Some npm packages ship a WASM module with a pure-JavaScript fallback for environments without WebAssembly. In a Hermes React Native context, the JavaScript fallback always activates. If the package’s security properties (timing resistance for cryptography, sandboxed execution for untrusted data processing) depend on the WASM implementation, those properties are absent in the RN deployment.
Hardening:
// Explicit capability detection before any WASM-dependent security operation
if (typeof WebAssembly === 'undefined' || typeof WebAssembly.instantiateStreaming === 'undefined') {
// Log to telemetry — this environment cannot run WASM
reportCapabilityGap('wasm-unavailable', { engine: navigator.userAgent });
// Do NOT silently fall back to JS for security-sensitive operations
throw new Error('WebAssembly is required for this operation. Upgrade your browser or app.');
}
PWA and WASM Security on Safari
Progressive Web Apps on iOS run in a Safari WebKit context. Safari’s PWA support has historically been more limited than Chrome’s, and several security-relevant differences apply:
Service worker scope: Service workers in Safari PWAs are limited to the origin of the PWA. A service worker cannot intercept requests to cross-origin CDN domains — which means the service-worker-based WASM integrity verification pattern described above only works for same-origin WASM module loads. WASM loaded from a CDN in a Safari PWA bypasses service worker verification.
Persistent storage limits: Safari aggressively evicts PWA storage (including service worker caches) when the device is under storage pressure. A service worker that caches WASM modules for offline use may find those modules evicted, forcing a re-download and re-verification on next use. This is a reliability concern, not a direct security vulnerability — but a re-download path that re-fetches a WASM module without proper integrity checks (because the developer assumed the cached version would always be present) is a vulnerability.
Push notification absence: Safari PWAs on iOS before iOS 16.4 did not support Web Push. This is irrelevant to WASM security directly, but it means PWA threat models on iOS cannot assume push-based revocation of compromised WASM module hashes — a mechanism some desktop PWA security designs use.
Home screen isolation: iOS PWAs added to the home screen run in their own process with a separate cookie jar and storage partition from Safari. This is a security feature — it prevents WASM-based tracking across the Safari session and the PWA session — but it also means session tokens, authentication cookies, and WASM-cached state are not shared. Applications that depend on shared state between the PWA and an embedded Safari tab for security decisions (e.g., shared logout) will find that state is not shared on iOS.
Hardening Checklist
Transport and integrity:
- Enforce HTTPS for all WASM module fetches; remove all
NSAllowsArbitraryLoadsandNSExceptionAllowsInsecureHTTPLoadsentries fromInfo.plistbefore production builds. - Add SRI
integrityattributes to all<script type="module">tags that load WASM wrappers. - Use a service worker to hash-verify WASM modules at the application layer for same-origin loads.
- Pin WASM module hashes in the service worker and update the pin as part of your release pipeline.
CSP configuration:
- Use
'wasm-unsafe-eval'inscript-src, not'unsafe-eval', for Safari 16+ deployments. - Set
Content-Security-Policyvia HTTP response header, not<meta>tag — the<meta>form does not apply to WASM in all WebKit versions. - Add
connect-srcrestrictions to prevent WASM modules from fetching arbitrary resources from WASM linear memory via imported JavaScript functions.
WKWebView bridge:
- Never pass user-controlled strings directly to
evaluateJavaScriptthat result in dynamic WASM compilation. - Validate all data crossing the
WKScriptMessageHandlerboundary with an explicit schema; reject unexpected fields. - Set
allowsContentJavaScript = falseonWKWebViewConfigurationfor webviews that do not need JavaScript; use separateWKWebViewinstances for different trust levels.
Memory management:
- Set a
maximumpage count onWebAssembly.Memorydescriptors; do not leave memory growth unbounded on mobile. - Test WASM module memory allocation failure paths explicitly; handle
WebAssembly.RuntimeErrorfrom OOM without leaving the application in a partial state.
Capability detection:
- Check
typeof WebAssembly !== 'undefined'before any WASM-dependent security operation. - Do not silently substitute a JavaScript fallback for security-sensitive WASM code; fail explicitly and log the capability gap.
React Native:
- Document explicitly whether a WASM module is intended to run in Hermes or in a WKWebView embedded in the RN app.
- Audit any WASM-with-JS-fallback npm packages for whether the fallback maintains the same security properties as the WASM implementation.
Summary
WebKit and iOS introduce a distinct set of WASM security constraints that V8-focused guidance does not address. BBQ-only execution in restricted WKWebView contexts limits JIT attack surface but changes performance assumptions. ATS provides transport-layer protection for WASM module fetches in iOS apps but does not cover integrity — combining it with SRI and service-worker hash pinning is required. WebKit’s conservative SharedArrayBuffer posture means threading code tested on Chrome may behave differently on Safari, in both directions. The WKWebView JS-native bridge is an injection risk when user-controlled data flows through it to WASM instantiation. Streaming compilation requires service-worker-based verification for same-origin modules because hash checking cannot happen inline. Hermes’s lack of WASM support means React Native deployments must not assume WASM sandbox properties in Hermes contexts.
The common failure mode across all of these: treating WebKit/Safari as a slower Chrome and assuming V8 hardening guidance transfers directly. It does not — the execution model, the platform constraints, and the Spectre mitigation choices are different enough to require a WebKit-specific security review for any WASM deployment that includes iOS in its target platforms.