Skip to content

Android: request headers persist in the environment and leak into the next request #324

Description

@inoha-kudo

What were you trying to do?

Serve a normal Laravel app from the embedded runtime on Android, where different requests carry different headers — an XHR that sends X-Requested-With and a top-level navigation that does not, a POST that sends Content-Type and a GET that does not.

What happened?

Every request sees the headers of the request before it.

Headers are passed to PHP as process environment variables (PHPBridge.handleLaravelRequest() sets HTTP_* via nativeSetEnv), and the environment outlives a dispatch in the persistent runtime. Nothing removes them, so $_SERVER in request N contains the union of every header seen since the process started, with the values from whichever request last sent each one.

Two things keep the existing defences from covering this:

  1. PHPBridge.kt:324 clears five names — the Inertia ones — and clears them by setting "" rather than removing them. Every other header keeps its previous, non-empty value.
  2. Clear stale HTTP_* headers from $_SERVER between persistent PHP dispatches #119 added a $_SERVER sweep to the dispatch eval (php_bridge.c:553), but the eval re-imports the environment twenty lines later (php_bridge.c:575, foreach (getenv() as $__k => $__v) { $_SERVER[$__k] = $__v; }), so the stale keys come straight back. $_SERVER cannot be cleaned while the environment it is rebuilt from is not.

Expected: $_SERVER['HTTP_X_FOO'] is absent on a request that did not send X-Foo.

iOS does not have this problem — PersistentPHPRuntime.swift records the keys it sets and unsetenvs them after each dispatch.

How to reproduce the bug

No backend, no Inertia — a stock app, one route, three fetch() calls.

// routes/web.php
Route::get('/leak', fn () => response($_SERVER['HTTP_X_FOO'] ?? '(absent)'));
await (await fetch('/leak', { headers: { 'X-Foo': 'bar' } })).text()
await (await fetch('/leak')).text()
await (await fetch('/leak')).text()

Release build, 4.1.0, physical device:

request header sent $_SERVER['HTTP_X_FOO'] expected
1 X-Foo: bar bar bar
2 (none) bar (absent)
3 (none) bar (absent)

Config: default runtime mode (persistent), embedded PHP 8.5.9.

Not visible under Jump — forwardToRemote() proxies to the dev server and never enters a PHP dispatch, so no header ever becomes an environment variable.

Debug Output

Package Version    | 4.1.0
PHP Version (Host) | 8.5.0
OS                 | Darwin
OS Version         | 25.6.0
Embedded PHP       | 8.5.9
Installed Plugins  | None

Xcode          | Xcode 26.3
Android Studio | Android Studio AndroidStudio2026.1.3
Gradle         | Gradle 8.14.5
Java           | openjdk version "17.0.14" 2025-01-21 LTS
CocoaPods      | Not found

Which operating systems have you seen this occur on?

macOS

Which platforms were you trying to build for?

Android (Device)

Notes

Device: Pixel 4a (5G), Android 14, arm64-v8a, WebView 150.0.7871.183. Built with native:package android --build-type=release, installed with adb install. NATIVEPHP_RUNTIME_MODE left at its default (persistent).

The user-visible form of this is what #119 set out to fix: an Inertia XHR sets HTTP_X_INERTIA, and the next top-level navigation inherits it and renders the page object as raw JSON. #119 fixed the $_SERVER half; the environment half remains, and it affects every header, not only the Inertia five.

A PR follows that tracks the header env vars each request sets and removes them after the dispatch — the same thing iOS already does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions