Skip to content

Block OTA bridge calls from the web view - #656

Merged
simonhamp merged 1 commit into
mainfrom
harden/block-ota-from-webview
Nov 4, 2026
Merged

simonhamp merged 1 commit into
mainfrom
harden/block-ota-from-webview

Conversation

@simonhamp

Copy link
Copy Markdown
Member

Refuses the Ota.* bridge methods from the web view, the same way AsyncTask.* is refused.

Ota.Download takes a caller-supplied URL and stages a zip that core applies on the next boot with no signature check, so a web view page (an XSS, or a bundled third-party script) calling it through /_native/api/call could stage an attacker bundle and have it run as the app's code next launch. OTA is app logic, not a page action — app PHP drives it in-process via nativephp_call, which doesn't pass through this controller — so the Ota. prefix is now dropped at the web view boundary.

Pairs with the nativephp/mobile-ota change that removes that plugin's page-facing JS API, so OTA is PHP-only on both sides. Found during the post-retest plugin sweep. Ships with the Composer package (no reinstall needed). Tests cover Ota.Download/Apply/Rollback.

🤖 Generated with Claude Code

Ota.Download takes a caller-supplied URL and stages a zip that core applies on
the next boot with no signature check, so a web view page (an XSS, or a bundled
third-party script) calling it through /_native/api/call could stage an
attacker bundle and get it run as the app's code next launch. OTA is app-logic,
not a page action — app PHP drives it in-process via nativephp_call, which does
not pass through this controller — so refuse the Ota. prefix from the web view
the same way AsyncTask. is refused. The plugin's page-facing JS API is being
removed to match. Found during the post-retest plugin sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@simonhamp
simonhamp merged commit 4774452 into main Nov 4, 2026
6 checks passed
@simonhamp
simonhamp deleted the harden/block-ota-from-webview branch November 4, 2026 12:45
simonhamp added a commit that referenced this pull request Nov 5, 2026
* Authenticate native device events so a web view page can't forge them

/_native/api/events has no middleware, so a page can POST to it itself. The
3.3.11 marker gate already stops it from constructing an arbitrary class, but a
page could still fire a real device event — a forged PaymentCompleted, a
Biometric\Completed(success: true), a scanner or camera result — and run the
app's listeners as if the device sent it.

The native shell now signs every event it posts there with the app key (held in
the Keychain/Keystore, never handed to the page) over a per-event nonce, a
timestamp and the exact request body. PHP recomputes the HMAC with the same key
and rejects anything stale, replayed or unsigned, then fires the event. The
shell builds the body by concatenation and posts it verbatim, so the bytes it
signs are exactly what PHP reads back as request()->getContent().

Enforcement is gated on NATIVEPHP_EVENTS_SIGNED, a native setenv on the embedded
PHP set only at on-device shell boot (iOS NativePHPApp.setupEnvironment, Android
LaravelEnvironment). It never reaches a shared PHP bootstrap or a dev-machine
PHP, so those stay unsigned and enforcement stays off there. An app that updates
only the composer package without rebuilding keeps working unsigned until it
reinstalls the native project.

The PHP verifier (Support/NativeEventSignature.php) is byte-identical to the 4.x
line, and the two native signers match its purpose string, message layout and
header names. Backport of #653. Reported by acd421 in a retest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Harden the Android web view: gate remote debugging and origin-scope AndroidPOST

Two residuals acd421 flagged in the retest, both in WebViewManager.

setWebContentsDebuggingEnabled(true) was unconditional, so release builds
shipped with chrome://inspect debugging on — anyone with device or ADB access
could inspect and drive the web view in production. Gate it on the app's
debuggable flag so it's on for debug builds and off for release.

addJavascriptInterface exposes the AndroidPOST bridge to every frame in the web
view, including any cross-origin iframe the app loads, so a foreign frame could
poison the stored CSRF token or feed body data to PHP through the
request-interception plumbing. Move AndroidPOST to WebViewCompat
.addWebMessageListener scoped to the origins the app actually serves pages from
(http://127.0.0.1 and http://localhost), so only first-party content reaches it.
The page now talks to the bridge via AndroidPOST.postMessage(JSON); a shared
helper sends through either transport, and the request-body shim's existing
50ms wait in PHPBridge.consumePostData absorbs postMessage's async delivery on
the fetch/XHR/form paths. WebViews too old for WEB_MESSAGE_LISTENER keep
addJavascriptInterface with a logged warning, since refusing the bridge there
would break every form and non-GET request.

Backport of #654 and #655.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Block OTA bridge calls from the web view

Ota.Download takes a caller-supplied URL and stages a zip that core applies on
the next boot with no signature check, so a web view page (an XSS, or a bundled
third-party script) calling it through /_native/api/call could stage an attacker
bundle and get it run as the app's code next launch. OTA is app-logic, not a
page action — app PHP drives it in-process via nativephp_call, which does not
pass through this controller — so refuse the Ota. prefix from the web view with
a 403.

3.x has no AsyncTask internal methods, so the refused-prefix list is just
['Ota.']. The gate ships with the composer package, independent of whether the
app has rebuilt its native project. Backport of #656; found during the
post-retest plugin sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant