fix: route editor REST requests through a native proxy under iOS Lockdown Mode - #544
Draft
jkmassel wants to merge 2 commits into
Draft
fix: route editor REST requests through a native proxy under iOS Lockdown Mode#544jkmassel wants to merge 2 commits into
jkmassel wants to merge 2 commits into
Conversation
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/544")Built from 8c77be2 |
…down Mode # Conflicts: # ios/Sources/GutenbergKit/Sources/EditorViewController.swift # ios/Sources/GutenbergKit/Sources/Model/GBKitGlobal.swift # src/utils/api-fetch.js
jkmassel
force-pushed
the
jkmassel/lockdown-mode-file-uploads
branch
from
August 14, 2026 18:05
f933707 to
a999b0d
Compare
jkmassel
force-pushed
the
jkmassel/lockdown-mode-file-uploads
branch
from
August 14, 2026 18:45
a999b0d to
8c77be2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the editor's REST API traffic failing with "Could not get a valid response from the server." when iOS Lockdown Mode is enabled. Ref CMM-2014.
RestRelay— a request handler on the native media upload server from feat: proxy media uploads through native delegate for processing #357 that relays the editor's REST API requests throughURLSession{port, token}to the editor viawindow.GBKit.networkProxywindow.fetchwrapper plus ajQuery.ajaxPrefilter— so uploads, link search, embeds, and any other in-editor REST traffic work under Lockdown ModeRoot Cause
The editor is a
file://page (loadFileURL), and every REST request it makes is a cross-originfetch()that normally bypasses CORS via theallowUniversalAccessFromFileURLspreference. Lockdown Mode stops honoring that exemption while the preference still changes how WebKit serializes the page's origin: requests go out withOrigin: file://.WordPress core sanitizes the echoed origin in
rest_send_cors_headers()throughesc_url_raw(), whose protocol allowlist does not includefile— so both WP.com and self-hosted sites respond with an emptyAccess-Control-Allow-Origin. WebKit rejects the response, and api-fetch surfaces its genericfetch_error— the exact message in the user reports.Verified on an iPhone 15 Pro (iOS 26.5) with system Lockdown Mode enabled, using a logging echo server to observe the wire:
Origin: file://(current editor)Origin: null'null')Origin: file://, echoed verbatim by the serverTwo corollaries worth knowing: requests do reach the server — only the response is discarded — so a "failed" upload can still create an attachment; and endpoints answering an unconditional
Access-Control-Allow-Origin: *keep working, which is why the editor otherwise appears functional.What We Explored
WKWebpagePreferences.isLockdownModeEnabled = false) crashes withNSInternalInconsistencyException: iOS requires the restrictedcom.apple.developer.web-browserentitlement, which only browsers can hold.Origin: null, which WordPress accepts — verified on device — but the editor fails to boot: Vite's dynamicimport()offile://chunks requiresallowFileAccessFromFileURLs.Origin: gbk-probe://probe-host, which the same sanitization reduces to an empty header. Same failure asfile://.Content-Disposition) changes nothing — the failure is response-side CORS, not the body.Fix
The web view fetches
http://127.0.0.1:<port>and native code performs the real request — the local server's permissive CORS policy (Access-Control-Allow-Origin: *, verified accepted by WebKit under Lockdown Mode for these non-credentialed requests) replaces the CORS negotiation WordPress fails.This builds on the media upload server that #357 introduced and #561 hardened, rather than running a second loopback server:
urlquery parameter falls under the configuredsiteApiRoot(the upstream URL rides in the query string so the HTTP library's stock CORS policy covers the preflight), streams disk-buffered bodies, and injects the configuredAuthorizationwhile discarding any client-supplied value. It also strips the upstream response's CORS headers — load-bearing, because the library adds its own withaddingHeadersIfAbsent, and WordPress's emptyAccess-Control-Allow-Originwould otherwise survive. Redirects are re-checked against the samesiteApiRootallowlist (a per-taskRedirectGuard), so a 3xx can't carry the injectedAuthorizationto another host./proxyrequests to the relay;/uploadand everything else is unchanged.defaultWebpagePreferences.isLockdownModeEnabledat web view creation).GBKit.nativeUploadPortis still gated on the delegate; the newGBKit.networkProxyis gated on Lockdown Mode — the two JS code paths cannot activate each other by accident. Also gains aGUTENBERG_FORCE_LOCKDOWN_MODE=1debug hook, since Lockdown Mode's web view restrictions can be forced per-view — this is how the fix is testable in the Simulator.window.fetchwrapper (createNetworkProxyFetch) that routes site requests through the relay at the layer below@wordpress/api-fetch's default handler — so it sees the fully-serialized request (data→body,Content-Type,per_page=-1pagination, abortsignal) and reuses api-fetch's own response parsing. AnapiFetch.use()middleware can't do this: it always runs outside the built-in handler that finalizes the request.jQuery.ajax/wp.ajax(XHR, invisible to the fetch wrapper) get a companionjQuery.ajaxPrefilter(createNetworkProxyAjaxPrefilter). Both are gated onGBKit.networkProxyand prefer the relay after the first success; media uploads keep their priority routing throughnativeMediaUploadMiddleware→/uploadfrom feat: proxy media uploads through native delegate for processing #357.networkProxypayload.The relay's
/proxyURL parsing was fixed.RestRelayread the upstreamurlfrom the request query throughURLComponents.percentEncodedQuery, butParsedHTTPRequest.queryincludes the leading?— which made the first query item?url, so the lookup forurlnever matched and every/proxyrequest returned a 400. It never surfaced on device: validation ran against the/uploadpipeline and the older header-based design, never/proxywith the?url=form. Fixed by stripping the leading?; covered byRestRelayTests.Nothing changes outside Lockdown Mode. Without a delegate the server didn't start before and still doesn't; with one, it behaves exactly as on trunk. The middleware is a no-op when
GBKit.networkProxyis absent.The relay is not a general proxy. It is reachable only via loopback with the per-session bearer token, refuses upstream URLs outside the site's API root, and strips client-supplied
Authorizationin favor of the natively-held credential.Raw
XMLHttpRequestis not relayed — yet. The relay coversfetchandjQuery.ajax/wp.ajax— the only request paths the editor core and its bundled/plugin scripts actually use. A direct, non-jQueryXMLHttpRequestwould still bypass the relay and fail under Lockdown Mode; nothing exercises that path today, so it's left uncovered until something does.Test plan
wp.apiFetchPOST /wp/v2/mediawith multipartFormData) created an attachment on a local wp-env site through the relay after the direct fetch rejected. Notably, the wp-env Playground server sends no usable CORS headers to any origin, so this exercises the worst-case server. (Validated on the pre-rebase branch; the post-rebase revalidation below repeats this end-to-end.)isLockdownModeEnabled = truereproduces the device's lockdown fingerprint cell-for-cell (WebAssembly/FileReaderremoved, CORS enforcement flipsno-corsresponses frombasictoopaque, direct REST rejects, relay carries the upload).swift test— all 24MediaUploadServertests pass with the relay routing in place; JS suite passes including the feat: proxy media uploads through native delegate for processing #357 middleware tests.swift test --filter RestRelay— 15 integration tests for the relay itself:/proxyURL parsing + SSRF refusal, forwarding,Authorizationinjection + client-auth stripping, request/response header + CORS stripping, the redirect guard, and 502 on an unreachable upstream.npm testcovers thewindow.fetchwrapper andjQuery.ajaxPrefilter./wp/v2/categories) succeed; direct fetches still reject (real WP sanitizesOrigin: file://to an empty ACAO).GutenbergKitOSLog subsystem), and uploads behave as on trunk./uploadwith native processing exactly as on trunk.Before review
RestRelay(upstream-URL refusal,Authorizationreplacement, upstream CORS-header stripping) and the JS middleware (fallback, sticky preference, parse semantics)LockdownModeSheetcopy should soften now that REST traffic works —WebAssembly/FileReader-dependent features remain degraded (tracked with CMM-2014)Related issues