docs(browse): only host:port was Chromium's own reading; user:pass@host:port is newly proxied

Chromium's proxy spec parser (net/base/proxy_string_util.cc, ProxySchemeHostAndPortToProxyServer) returns an invalid server for any userinfo, so a scheme-less user:pass@host:port in HTTPS_PROXY was never honoured by Chromium on Linux and went direct; reading it as http:// follows curl and is new behaviour. BROWSER.md, the CHANGELOG, the resolveProxy docblock and the Linux e2e test comment now say so, and the scheme-less spill test is named for what it proves (those values never reach the path refusal; the http:// cases in the next test do). Wording only.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 06:14:58 +00:00
1 parent 5203975717
commit 15fafe0c51
5 files changed
+16 -11

No files matched your search

+8 -4
View File
@@ -1002,12 +1002,16 @@ asked for, not what the policy enforces.
Values from the environment follow the same credential policy as `--proxy`.
A variable without a scheme (curl-style `host:port` or `user:pass@host:port`)
is read as `http://host:port` (port 80 when none is given), the way
Chromium's own environment reader took it, so a value Chromium on Linux used
to pick up by itself now reaches the same proxy through `$B`; on macOS and
is read as `http://host:port` (port 80 when none is given). For `host:port`
that is what Chromium's own environment reader took it as, so a value
Chromium on Linux used to pick up by itself now reaches the same proxy
through `$B`; `user:pass@host:port` follows curl's reading and is newly
proxied, because Chromium rejects any proxy spec with a userinfo. On macOS and
Windows such a value is now a configured proxy too, where it used to be
ignored with a warning in favour of the system proxy settings. `--proxy` and
`BROWSE_PROXY_URL` still require the scheme. `socks5h://` is treated as `socks5://`, because Chromium's SOCKS5 already
`BROWSE_PROXY_URL` still require the scheme.
`socks5h://` is treated as `socks5://`, because Chromium's SOCKS5 already
resolves names through the proxy. `NO_PROXY` (or `no_proxy`) is translated to
Chromium's bypass list: `example.com` and `.example.com` cover the domain and
its subdomains, and CIDR blocks and `host:port` entries pass through.
+1 -1
View File
@@ -6,7 +6,7 @@
### Fixed
- **`browse/src/browser-manager.ts`**: each of `BrowserManager`'s four Chromium launch sites (headless `launch`, the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded`, `handoff`) adds `--proxy-server=direct://` when no proxy is configured and `forcesDirect()` says Chromium would otherwise pick one browse did not resolve, from one `proxyLaunchArgs()`. A configured proxy was already explicit (Playwright passes it as `--proxy-server`), so the environment can no longer win silently in either mode. `--no-proxy-server` was measured and rejected: chrome-headless-shell, the default headless build, ignores it and still took `HTTPS_PROXY`; the full Chromium honors both switches, and `direct://` works on both. The cookie-import worker (`cookie-import-native-worker.ts`), which launches the user's own browser only to read its cookie store and never navigates, is unchanged.
- **`browse/src/proxy-config.ts`**: `forcesDirect(env, platform)` is true on Linux always, where the leak lives, and on macOS and Windows only with `BROWSE_PROXY_URL=direct`. Chromium there never read the proxy variables and follows the system proxy settings (PAC and WPAD included), so a user of GStack Browser.app behind a corporate proxy keeps that connectivity, and a PAC cannot be named through `BROWSE_PROXY_URL`. An explicit `direct` bypasses the system settings on every platform. A standard proxy variable without a scheme (curl-style `host:port` or `user:pass@host:port`) is now read as an `http://` proxy (port 80 when none is given), the way Chromium's own environment reader took it: before this change browse rejected it with a warning and Chromium on Linux read the variable anyway, so that session was proxied; with the launch now explicit, reading it the same way keeps those sessions on their proxy instead of sending them direct. On macOS and Windows the same value is now a configured proxy too, where it used to be ignored in favour of the system proxy settings, so a daemon started under the old warning meets the mismatch refusal once. Only the plain `[user:pass@]host[:port]` shape is read that way; a value with a scheme (including the slash-less `https:user:pass@host:port`, which the URL parser reads as `https://`), a `/`, `\`, `?`, `#`, `@` or whitespace anywhere else in the value, or a bare number goes to the URL parser as written, so a bare number such as `1234` keeps its diagnostic and a one-slash spelling such as `http:/host:3128` is read as the URL parser always read it (`http://host:3128/`). A proxy URL with a path other than a bare `/`, a query or a fragment (a literal `?` or `#` anywhere) is now refused for every source (`--proxy` included), because that is where a `DOMAIN\user` login or a `/` in a password lands after URL parsing and only the userinfo is redacted; percent-encode `/`, `?` and `#` in credentials for every scheme and `\` in `http://`/`https://` credentials (a `socks5://` userinfo keeps a backslash as written). This narrows the environment path: a credential-free value with a path, such as `http://proxy.corp:3128/pac`, used to be read with the path dropped and the session proxied; it now warns and goes direct like any other unusable standard-variable value, and `--proxy`/`BROWSE_PROXY_URL` refuse it. Credentials that are not valid percent-encoding (a literal `%`) are a proxy configuration error instead of a crash, for every source. For a standard variable each of these parse problems is a warning and a direct connection, like any other unusable value; a scheme-less value with credentials beside `BROWSE_PROXY_USER`/`BROWSE_PROXY_PASS` is now refused like its `http://` spelling, which has always been a hard error (that value used to warn and go direct because it did not parse). A value that does not parse even as `http://` (an unsupported scheme, a bad port, a missing host) still warns and connects directly; `--proxy` and `BROWSE_PROXY_URL` still require the scheme.
- **`browse/src/proxy-config.ts`**: `forcesDirect(env, platform)` is true on Linux always, where the leak lives, and on macOS and Windows only with `BROWSE_PROXY_URL=direct`. Chromium there never read the proxy variables and follows the system proxy settings (PAC and WPAD included), so a user of GStack Browser.app behind a corporate proxy keeps that connectivity, and a PAC cannot be named through `BROWSE_PROXY_URL`. An explicit `direct` bypasses the system settings on every platform. A standard proxy variable without a scheme (curl-style `host:port` or `user:pass@host:port`) is now read as an `http://` proxy (port 80 when none is given). For `host:port` that is what Chromium's own environment reader took it as: before this change browse rejected it with a warning and Chromium on Linux read the variable anyway, so that session was proxied; with the launch now explicit, reading it the same way keeps those sessions on their proxy instead of sending them direct. `user:pass@host:port` follows curl's reading and is newly proxied: Chromium rejects any proxy spec that carries a userinfo (`net/base/proxy_string_util.cc`), so that session was direct before and now sends its credentials to the named proxy. On macOS and Windows the same value is now a configured proxy too, where it used to be ignored in favour of the system proxy settings, so a daemon started under the old warning meets the mismatch refusal once. Only the plain `[user:pass@]host[:port]` shape is read that way; a value with a scheme (including the slash-less `https:user:pass@host:port`, which the URL parser reads as `https://`), a `/`, `\`, `?`, `#`, `@` or whitespace anywhere else in the value, or a bare number goes to the URL parser as written, so a bare number such as `1234` keeps its diagnostic and a one-slash spelling such as `http:/host:3128` is read as the URL parser always read it (`http://host:3128/`). A proxy URL with a path other than a bare `/`, a query or a fragment (a literal `?` or `#` anywhere) is now refused for every source (`--proxy` included), because that is where a `DOMAIN\user` login or a `/` in a password lands after URL parsing and only the userinfo is redacted; percent-encode `/`, `?` and `#` in credentials for every scheme and `\` in `http://`/`https://` credentials (a `socks5://` userinfo keeps a backslash as written). This narrows the environment path: a credential-free value with a path, such as `http://proxy.corp:3128/pac`, used to be read with the path dropped and the session proxied; it now warns and goes direct like any other unusable standard-variable value, and `--proxy`/`BROWSE_PROXY_URL` refuse it. Credentials that are not valid percent-encoding (a literal `%`) are a proxy configuration error instead of a crash, for every source. For a standard variable each of these parse problems is a warning and a direct connection, like any other unusable value; a scheme-less value with credentials beside `BROWSE_PROXY_USER`/`BROWSE_PROXY_PASS` is now refused like its `http://` spelling, which has always been a hard error (that value used to warn and go direct because it did not parse). A value that does not parse even as `http://` (an unsupported scheme, a bad port, a missing host) still warns and connects directly; `--proxy` and `BROWSE_PROXY_URL` still require the scheme.
- **`browse/src/cli.ts`, `browse/src/server.ts`**: on macOS and Windows the explicit `direct` is part of the daemon config hash (`explicitDirect()`), is recorded in the daemon state as `proxyDirect` and is re-emitted on a crash auto-restart from the session's own flags, so a running daemon started on the system proxy is refused rather than reused by a session that opted out (the refusal names `BROWSE_PROXY_URL` and offers `browse stop` or unsetting it; a daemon started before this release meets that refusal once, and a daemon with no recorded config hash is refused like one started without `--proxy`; the mismatch refusal offers `BROWSE_PROXY_URL=direct` as a remedy only where it would match the running daemon, on Linux or against a daemon started with it, and otherwise says to unset the variable that differs). The unparseable-variable warning's hint now says that `BROWSE_PROXY_URL=direct` also bypasses the system proxy settings on macOS and Windows. On Linux the opt-out hashes like no proxy, as before, because every direct connection there already carries the switch.
- **`browse/src/server.ts`** logs the effective proxy mode each time the daemon starts, beside the existing `[proxy] using <url> from <source>` line: `[browse] [proxy] none: direct connection (--proxy-server=direct://; ignoring HTTPS_PROXY)` on Linux, naming each standard variable that was set and ignored; `(...; ignoring the system proxy settings)` for an explicit direct on macOS and Windows; and `[browse] [proxy] none: system proxy settings (Chromium's default on <platform>; BROWSE_PROXY_URL=direct connects directly)` when those platforms are left to the system.
- **BROWSER.md, "Proxy from environment"**: says that whatever `$B` resolves is passed to Chromium explicitly, that on Linux a set proxy variable is ignored in direct mode, that macOS and Windows follow the system proxy settings unless `BROWSE_PROXY_URL=direct` is set (and that the opt-out is part of the daemon's settings there), where the daemon log records the effective mode, and that a managed Chromium proxy policy still overrides the switch. The eMudhra/TLS trust section is unchanged.
+5 -4
View File
@@ -344,10 +344,11 @@ const LEADING_SCHEME_RE = /^(?:https?|socks5h?):/i;
* Resolve the proxy for this invocation (#915). Env URLs go through
* parseProxyConfig, so the cred policy applies. A standard variable in a
* scheme-less shape (curl-style `host:port` or `user:pass@host:port`,
* SCHEMELESS_PROXY_RE) is read as an `http://` proxy, the way curl and
* Chromium's own environment reader take it, so a session that worked through
* Chromium reading the variable keeps working now that the launch is explicit
* (#3440). One that still does not parse (an unsupported scheme, a bad port,
* SCHEMELESS_PROXY_RE) is read as an `http://` proxy, the way curl takes it.
* For `host:port` that is also what Chromium's own environment reader did,
* so a session that worked through Chromium reading the variable keeps
* working now that the launch is explicit (#3440); `user:pass@host:port` is
* newly proxied, because Chromium rejects any proxy spec with a userinfo. One that still does not parse (an unsupported scheme, a bad port,
* a missing host) is ignored with a warning instead of failing every command;
* --proxy and BROWSE_PROXY_URL are explicit browse settings and still fail
* closed, scheme required.
+1 -1
View File
@@ -79,7 +79,7 @@ describe.skipIf(process.platform !== 'linux')('#3440 item 8: direct mode never l
}, 120_000);
// Added by: /ship test coverage audit (Capy), 2026-10-25, for #3440 item 8.
// Value: protects=a scheme-less HTTPS_PROXY (host:port, what Chromium's own environment reader took as an HTTP proxy before the launch was explicit) still reaches that proxy, now through browse's own configuration;
// Value: protects=a scheme-less HTTPS_PROXY (host:port, what Chromium's own environment reader took as an HTTP proxy before the launch was explicit; a user:pass@ shape is newly proxied, Chromium rejected it) still reaches that proxy, now through browse's own configuration;
// fails_when=resolveProxy stops reading scheme-less variables as http:// and the session goes direct with only a warning; why_new=proxy-env-daemon's scheme-less case targets loopback, which Chromium bypasses anyway; seam=none
test('a scheme-less HTTPS_PROXY is the configured proxy and the CONNECT reaches the host:port Chromium would have used by itself', async () => {
const envProxy = await connectProxy();
+1 -1
View File
@@ -85,7 +85,7 @@ describe('resolveProxy precedence', () => {
expect(resolveProxy(null, { HTTPS_PROXY: '1234' }).warning).toStartWith('HTTPS_PROXY is not a proxy URL browse can use (');
});
test('a scheme-less password that would spill past the userinfo, or that is not percent-encoded, warns instead of leaking or crashing', () => {
test('a scheme-less value whose credential holds / \\ ? # or bad percent-encoding is not rewritten to http:// and warns without echoing the password or crashing', () => {
for (const value of ['bob:1234?x@proxy.corp:3128', 'bob:1234#x@proxy.corp:3128', 'bob:1234/x@proxy.corp:3128', 'CORP\\jdoe:1234@proxy.corp:3128', 'bob:\\1234@proxy.corp:3128', 'bob:1234\\abc@proxy.corp:3128']) {
const resolved = resolveProxy(null, { HTTPS_PROXY: value });
expect(resolved.url, value).toBeNull();