From 5c69190df67fa0f801b520471d7c83e3e391a0ce Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 1 Sep 2026 16:41:47 -0400 Subject: [PATCH 1/4] refactor: read the editor configuration from the injected global only `getGBKit` fell back to a copy of the configuration in `localStorage`. Boot waits for `window.GBKit` before anything reads the configuration, and outside `?dev_mode` aborts when it never arrives, so the fallback could only ever serve a previous session's values to a dev-mode page with no host. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV --- src/utils/bridge.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/utils/bridge.js b/src/utils/bridge.js index c2b70f843..855e3dc74 100644 --- a/src/utils/bridge.js +++ b/src/utils/bridge.js @@ -261,22 +261,13 @@ export const POST_FALLBACKS = { }; /** - * Retrieves the native-host-provided GBKit object from localStorage or returns - * an empty object if not found. + * Retrieves the native-host-provided GBKit object or returns an empty object + * if the host has not injected one. * * @return {GBKitConfig} The GBKit object. */ export function getGBKit() { - if ( window.GBKit ) { - return window.GBKit; - } - - try { - return JSON.parse( localStorage.getItem( 'GBKit' ) ) || {}; - } catch ( err ) { - error( 'Failed to parse GBKit from localStorage', err ); - return {}; - } + return window.GBKit || {}; } /** From b1f30d314648c795e30baffde96d6dfaf04fea73 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 1 Sep 2026 16:47:23 -0400 Subject: [PATCH 2/4] fix(ios): stop persisting the editor configuration `GBKit` carries the site credential and the local server's port and tokens, all valid only for the load that injected them, and iOS mirrored it into `localStorage`, which the default website data store keeps on disk across launches. The document-start user script replays the global on every navigation, including the reload after a WebContent process termination, so the copy had no reader. Remove the key as the configuration is injected so devices upgraded from an earlier version are scrubbed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV --- .../Sources/EditorViewController.swift | 23 +++++++++++----- .../EditorConfigurationScriptTests.swift | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 ios/Tests/GutenbergKitTests/EditorConfigurationScriptTests.swift diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index c03a64dba..b944de211 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -467,15 +467,26 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro nativeUploadToken: isUploadPipelineEnabled ? uploadServer?.token : nil, networkProxy: networkProxyGlobal ) - let stringValue = try gbkitGlobal.toString() + return WKUserScript( + source: Self.configurationScript(gbkitGlobal: try gbkitGlobal.toString()), + injectionTime: .atDocumentStart, + forMainFrameOnly: true + ) + } - let jsCode = """ - window.GBKit = \(stringValue); - localStorage.setItem('GBKit', JSON.stringify(window.GBKit)); + /// The document-start script that installs `window.GBKit`. + /// + /// The configuration is session-scoped — it carries the site credential and + /// the local server's port and tokens — so no copy of it outlives the load + /// that injected it. Earlier versions mirrored it into `localStorage`, which + /// the default website data store keeps on disk across launches; the script + /// removes that key so a device upgraded from one of them is scrubbed. + static func configurationScript(gbkitGlobal: String) -> String { + """ + window.GBKit = \(gbkitGlobal); + localStorage.removeItem('GBKit'); "done"; """ - - return WKUserScript(source: jsCode, injectionTime: .atDocumentStart, forMainFrameOnly: true) } /// Starts the local HTTP server for routing file uploads through native processing. diff --git a/ios/Tests/GutenbergKitTests/EditorConfigurationScriptTests.swift b/ios/Tests/GutenbergKitTests/EditorConfigurationScriptTests.swift new file mode 100644 index 000000000..1115dd931 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/EditorConfigurationScriptTests.swift @@ -0,0 +1,26 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +#if canImport(UIKit) + +@Suite("Editor configuration script") +struct EditorConfigurationScriptTests { + + @MainActor + @Test("injects the configuration without persisting it") + func doesNotPersistTheConfiguration() { + // The injected configuration carries the site credential and the local + // server's port and tokens, all of them valid only for this session. + let script = EditorViewController.configurationScript( + gbkitGlobal: #"{"authHeader":"Bearer secret"}"# + ) + + #expect(script.contains(#"window.GBKit = {"authHeader":"Bearer secret"};"#)) + #expect(script.contains("localStorage.removeItem('GBKit')")) + #expect(!script.contains("localStorage.setItem")) + } +} + +#endif From db896388c9d239b640ef752f99dafd79eb0cca07 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 1 Sep 2026 16:48:59 -0400 Subject: [PATCH 3/4] fix(android): stop persisting the editor configuration `GBKit` carries the site credential and the local server's port and token, all valid only for the load that injected them. The view re-injects the global on every page start and wipes web storage before each load, so the `localStorage` copy had no reader and nothing left to clear on detach. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV --- .../java/org/wordpress/gutenberg/GutenbergView.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index d893265f4..744d6029d 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -667,12 +667,8 @@ class GutenbergView : FrameLayout { nativeUploadToken = uploadServer?.token ) val gbKitJson = gbKit.toJsonString() - val gbKitConfig = """ - window.GBKit = $gbKitJson; - localStorage.setItem('GBKit', JSON.stringify(window.GBKit)); - """.trimIndent() - webView.evaluateJavascript(gbKitConfig, null) + webView.evaluateJavascript("window.GBKit = $gbKitJson;", null) } private fun startUploadServer() { @@ -726,12 +722,7 @@ class GutenbergView : FrameLayout { } fun clearConfig() { - val jsCode = """ - delete window.GBKit; - localStorage.removeItem('GBKit'); - """.trimIndent() - - webView.evaluateJavascript(jsCode, null) + webView.evaluateJavascript("delete window.GBKit;", null) } fun setContent(newContent: String) { From f32075c68f64aacae380114c36d0c8209d8f790a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 1 Sep 2026 16:49:24 -0400 Subject: [PATCH 4/4] docs: describe where the local server's token lives The CORS rationale on both platforms named `localStorage` alongside `window.GBKit` as where the editor holds the per-session bearer token. The token now lives in the injected global only. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV --- .../src/main/java/org/wordpress/gutenberg/HttpServer.kt | 8 ++++---- ios/Sources/GutenbergKitHTTP/CORSPolicy.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/HttpServer.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/HttpServer.kt index fefd16a3e..971f78050 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/HttpServer.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/HttpServer.kt @@ -114,10 +114,10 @@ enum class CorsPolicy { // `*` (any origin) rather than echoing a specific origin is // deliberate, and safe here — not an oversight to tighten. The // server is loopback-only, and every non-OPTIONS request is gated - // by a per-session random bearer token stored only in the editor - // origin's localStorage/window.GBKit, which is origin-scoped and - // unreadable by any other origin — so no cross-origin can obtain - // it. `*` only governs whether a *token-holding* origin may read + // by a per-session random bearer token held only in the editor + // origin's window.GBKit, which is origin-scoped and unreadable + // by any other origin — so no cross-origin can obtain it. `*` + // only governs whether a *token-holding* origin may read // the response, and the sole token-holder is the editor itself, the // legitimate client. Echoing the origin isn't viable anyway: the // editor loads from file:// (Origin null), which can't be cleanly diff --git a/ios/Sources/GutenbergKitHTTP/CORSPolicy.swift b/ios/Sources/GutenbergKitHTTP/CORSPolicy.swift index b6dabe7e8..631badd55 100644 --- a/ios/Sources/GutenbergKitHTTP/CORSPolicy.swift +++ b/ios/Sources/GutenbergKitHTTP/CORSPolicy.swift @@ -22,10 +22,10 @@ public enum CORSPolicy: Sendable { // `*` (any origin) rather than echoing a specific origin is // deliberate, and safe here — not an oversight to tighten. The // server is loopback-only, and every non-OPTIONS request is gated - // by a per-session random bearer token stored only in the editor - // origin's `localStorage`/`window.GBKit`, which is origin-scoped - // and unreadable by any other origin — so no cross-origin can - // obtain it. `*` only governs whether a *token-holding* origin may + // by a per-session random bearer token held only in the editor + // origin's `window.GBKit`, which is origin-scoped and unreadable + // by any other origin — so no cross-origin can obtain it. `*` + // only governs whether a *token-holding* origin may // read the response, and the sole token-holder is the editor // itself, the legitimate client. Echoing the origin isn't viable // anyway: the editor loads from `file://` and WebKit sends