Playwright 1.65.0: bundled Chromium 158 drops the distrusted eMudhra root (#3338) (#4087)

* Playwright 1.65.0: bundled Chromium 158 drops the distrusted eMudhra root (#3338)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: PROJECT_STRUCTURE patches note drops the TcpPortRandomizationWin backport

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog.d: mark the eMudhra fix important: security (#3338)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-06 17:33:41 +00:00
1 parent cb8f9eaeac
commit 02d3d965a6
13 files changed
+98 -54

No files matched your search

+9 -9
View File
@@ -1375,17 +1375,17 @@ Playwright's `--disable-component-update`, so it never receives the CRLSet
revocation updates Chrome pushes to running browsers. Aside uses your own
browser's trust store and updates.
**Known gap: the compromised eMudhra intermediates
**The compromised eMudhra intermediates
([#3338](https://github.com/garrytan/gstack/issues/3338)).** Chrome blocked
eight eMudhra intermediate CAs by CRLSet on 2026-10-17, the day their key
compromise was reported, and scheduled the removal of their root, emSign ECC
Root CA - G3, from the Chrome Root Store for 2026-10-27. The root store built
into gstack's pinned Playwright 1.64.1 (Chromium 156) still trusts that root, so
the headless browser accepts certificates those intermediates issued. That lasts
until gstack moves to the first Playwright release whose Chromium is
158.0.8115.0 or later, the first Chrome for Testing build measured to refuse
them. Until then, don't do signed-in work in this engine on a network you do not
trust, and prefer Aside for signed-in work where you have it.
compromise was reported, and removed their root, emSign ECC Root CA - G3, from
the Chrome Root Store. Playwright 1.64.1 (Chromium 156) still trusted that root,
so gstack's headless browser accepted certificates those intermediates issued.
gstack now pins Playwright 1.65.0, whose Chromium 158.0.8127.18 no longer
carries the root, so the headless shell and the full Chromium both refuse those
certificates. `./setup` downloads that Chromium on an upgraded install. A
`GSTACK_CHROMIUM_PATH` build older than 158.0.8115.0, the first Chrome for
Testing build measured to refuse them, still trusts the root.
To check a build, run it on a network that does not inspect TLS, because a
captive portal or an intercepting proxy can fail some or all sites with the
@@ -0,0 +1,35 @@
/**
* #3338: the browse daemon's default headless browser (chrome-headless-shell) and the full Chromium it uses
* for headed mode, handoff and extensions verify TLS against the Chrome Root Store compiled into the binary.
* Chrome removed emSign ECC Root CA - G3, the root over the eight eMudhra intermediates whose keys were
* compromised. Nobody holds that root's key, so no fixture chain can exercise it; this reads the installed
* binaries instead. A still-trusted root (ISRG Root X1) must be present, which proves the scan reads the root
* store and keeps a missing or relocated store from passing. Linux only: there both bundled browsers are single
* executables; macOS keeps the full browser's store in its framework.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import { chromium } from 'playwright';
const REMOVED_ROOT = 'emSign ECC Root CA - G3';
const TRUSTED_ROOT = 'ISRG Root X1';
async function headlessShellPath(): Promise<string> {
const server = await chromium.launchServer({ headless: true });
try {
return server.process().spawnfile;
} finally {
await server.close();
}
}
describe.skipIf(process.platform !== 'linux')('bundled Chromium root store (#3338)', () => {
test('neither bundled browser carries the removed eMudhra root', async () => {
for (const file of [await headlessShellPath(), chromium.executablePath()]) {
const binary = fs.readFileSync(fs.realpathSync(file));
expect({ file, trustedRoot: binary.includes(TRUSTED_ROOT), removedRoot: binary.includes(REMOVED_ROOT) })
.toEqual({ file, trustedRoot: true, removedRoot: false });
}
}, 30_000);
});
@@ -66,7 +66,7 @@ const nodeInfo = JSON.parse(nodeProbe.stdout);
if (nodeInfo.architecture !== process.arch || !['x64', 'arm64'].includes(process.arch)) incomplete('runtime_architecture_mismatch');
const require = createRequire(import.meta.url);
const playwrightVersion = require('playwright/package.json').version;
if (playwrightVersion !== '1.64.1') incomplete('playwright_1_64_1_required');
if (playwrightVersion !== '1.65.0') incomplete('playwright_1_65_0_required');
const versionProbe = spawnSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), [
'-NoProfile', '-NonInteractive', '-Command', NATIVE_BROWSER_VERSION_COMMAND,
], { env: { ...environment, GSTACK_QUALIFY_BROWSER_EXE: executable }, encoding: 'utf8', timeout: 10_000, windowsHide: true });
+1 -1
View File
@@ -213,7 +213,7 @@ async function qualifiedAdapterFixture() {
const receipt = {
browserName: 'Edge', architecture: runtime.architecture, windowsRelease: release(),
executableSha256: '0'.repeat(64), nodeVersion: runtime.version, bunVersion: Bun.version,
playwrightVersion: '1.64.1', sourceHashes,
playwrightVersion: '1.65.0', sourceHashes,
};
const virtualRoot = `C:\\gstack-${path.basename(fixture)}`;
const windowsEnv = { LOCALAPPDATA: `${virtualRoot}\\Local`, PROGRAMFILES: `${virtualRoot}\\Apps`, 'PROGRAMFILES(X86)': `${virtualRoot}\\Apps32` };
+22 -5
View File
@@ -63,11 +63,10 @@ describe('playwright-core windowsHide patch (#2160, #1989)', () => {
});
// Windows Free Tests 40656299153 and 41056150287 (cookie-picker-ui, serial retry passed): goto failed with
// net::ERR_NO_BUFFER_SPACE on 127.0.0.1. Chromium's TcpPortRandomizationWin raises WSAENOBUFS with ports free;
// microsoft/playwright#43605 disables it in 1.65, and the 1.64.1 patch backports it; drop this block at 1.65.
describe('playwright-core TcpPortRandomizationWin backport (microsoft/playwright#43605)', () => {
test('the patch and the installed tree disable the feature in the --disable-features list', () => {
expect(fs.readFileSync(path.join(ROOT, 'patches', 'playwright-core@1.64.1.patch'), 'utf-8')).toContain('+ "TcpPortRandomizationWin"');
// net::ERR_NO_BUFFER_SPACE on 127.0.0.1. Chromium's TcpPortRandomizationWin raises WSAENOBUFS with ports free.
// The 1.64.1 patch backported microsoft/playwright#43605; 1.65.0 ships it upstream, so the patch no longer carries it.
describe('playwright-core disables TcpPortRandomizationWin (microsoft/playwright#43605)', () => {
test('the installed tree disables the feature in the --disable-features list', () => {
const bundle = fs.readFileSync(path.join(ROOT, 'node_modules', 'playwright-core', 'lib', 'coreBundle.js'), 'utf-8');
const list = bundle.slice(bundle.indexOf('disabledFeatures = ['), bundle.indexOf('].filter(Boolean);', bundle.indexOf('disabledFeatures = [')));
expect(list).toContain('"TcpPortRandomizationWin"');
@@ -89,3 +88,21 @@ describe('bundled Chromium carries the CVE-2026-106378 fix (#3907)', () => {
expect(cmp).toBeGreaterThanOrEqual(0);
});
});
// #3338: Chrome removed emSign ECC Root CA - G3, the root over the eight eMudhra intermediates whose keys were
// compromised, from the Chrome Root Store. The headless shell gets no CRLSet, so only the compiled root store
// protects it. Chrome for Testing 158.0.8115.0 is the first build measured to refuse the root
// (ERR_CERT_AUTHORITY_INVALID on dv-g3b-emudhra.we-have-a-problem.com); Playwright 1.64.1's 156.0.8078.4 loads it.
describe('bundled Chromium no longer trusts the eMudhra root (#3338)', () => {
test('playwright-core pins Chromium and the headless shell at 158.0.8115.0 or later', () => {
const browsers = JSON.parse(fs.readFileSync(
path.join(ROOT, 'node_modules', 'playwright-core', 'browsers.json'), 'utf-8',
)) as { browsers: Array<{ name: string; browserVersion: string }> };
const floor = [158, 0, 8115, 0];
for (const name of ['chromium', 'chromium-headless-shell']) {
const version = browsers.browsers.find((b) => b.name === name)!.browserVersion.split('.').map(Number);
const cmp = version.map((part, i) => part - floor[i]).find((d) => d !== 0) ?? 0;
expect({ name, atOrAboveFloor: cmp >= 0 }).toEqual({ name, atOrAboveFloor: true });
}
});
});
+4 -4
View File
@@ -13,7 +13,7 @@
"html-to-docx": "1.8.0",
"html-to-vdom": "0.7.0",
"marked": "^18.1.1",
"playwright": "^1.64.1",
"playwright": "^1.65.0",
"sharp": "^0.35.4",
"socks": "^2.8.10",
"xterm": "^5.3.0",
@@ -28,7 +28,7 @@
},
},
"patchedDependencies": {
"playwright-core@1.64.1": "patches/playwright-core@1.64.1.patch",
"playwright-core@1.65.0": "patches/playwright-core@1.65.0.patch",
},
"overrides": {
"@hono/node-server": "2.1.6",
@@ -467,9 +467,9 @@
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
"playwright": ["playwright@1.64.1", "", { "dependencies": { "playwright-core": "1.64.1" }, "bin": { "playwright": "cli.js" } }, "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA=="],
"playwright": ["playwright@1.65.0", "", { "dependencies": { "playwright-core": "1.65.0" }, "bin": { "playwright": "cli.js" } }, "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA=="],
"playwright-core": ["playwright-core@1.64.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="],
"playwright-core": ["playwright-core@1.65.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA=="],
"prettier": ["prettier@3.9.10", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA=="],
@@ -0,0 +1,6 @@
---
important: security
---
### Security
- **Security fix: gstack's bundled browser no longer trusts the compromised eMudhra intermediates (#3338).** gstack now pins Playwright 1.65.0, which bundles Chromium 158.0.8127.18. Chrome removed emSign ECC Root CA - G3, the root over the eight eMudhra intermediate CAs whose keys were compromised, from the Chrome Root Store, but Playwright 1.64.1's Chromium 156 still carried it. gstack's default headless browser gets no CRLSet updates, so it accepted certificates those intermediates issued, and anyone holding the weak keys could impersonate a site to it. With 1.65.0, `$B goto https://dv-g3b-emudhra.we-have-a-problem.com/` fails with `ERR_CERT_AUTHORITY_INVALID` in both the headless shell and the full Chromium; on 1.64.1 it loaded the page. Run `./setup` after upgrading to download the new Chromium. `/setup-browser-cookies` drops its warning about this gap, and BROWSER.md says the gap is closed. The Windows windowsHide patch is re-cut for 1.65.0, and its backport of Playwright's TcpPortRandomizationWin fix is gone because 1.65.0 ships that fix itself. New free tests fail if the bundled Chromium is older than 158.0.8115.0 or still carries the root.
+1 -1
View File
@@ -98,7 +98,7 @@ gstack/
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide, TcpPortRandomizationWin disabled on Windows)
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide)
├── docs/designs/ # Design documents (incl. IMPECCABLE_INTEROP.md = the design detector / catalog / open DESIGN.md record, and fork-port-residual-2026-09/ evaluation evidence)
├── setup-deploy/ # /setup-deploy skill (one-time deploy config)
├── .github/ # CI workflows + shared composite actions (.github/actions/) + Docker image (claude CLI pinned)
+2 -2
View File
@@ -77,7 +77,7 @@
"html-to-docx": "1.8.0",
"html-to-vdom": "0.7.0",
"marked": "^18.1.1",
"playwright": "^1.64.1",
"playwright": "^1.65.0",
"sharp": "^0.35.4",
"socks": "^2.8.10",
"xterm": "^5.3.0",
@@ -115,6 +115,6 @@
"sharp": "0.35.5"
},
"patchedDependencies": {
"playwright-core@1.64.1": "patches/playwright-core@1.64.1.patch"
"playwright-core@1.65.0": "patches/playwright-core@1.65.0.patch"
}
}
-27
View File
@@ -1,27 +0,0 @@
diff --git a/lib/coreBundle.js b/lib/coreBundle.js
index 6893f1cf133efec2c5dccc35c77cc7a53870ccbc..7990e202c5b574ca5ea4c3e73c15c2da43a5b980 100644
--- a/lib/coreBundle.js
+++ b/lib/coreBundle.js
@@ -10031,6 +10031,10 @@ async function launchProcess(options) {
env: options.env,
cwd: options.cwd,
shell: options.shell,
+ // gstack patch (#2160/#1989): Node defaults windowsHide to false for
+ // child_process.spawn, so browser children could flash a console window
+ // on Windows. Playwright never needs the child's window shown.
+ windowsHide: true,
stdio
};
const spawnedProcess = childProcess.spawn(options.command, options.args || [], spawnOptions);
@@ -36204,7 +36208,10 @@ var init_chromiumSwitches = __esm({
// Disables forced sign-in in Edge.
"msForceBrowserSignIn",
// Disables updating the preferred version in LaunchServices preferences on mac.
- "msEdgeUpdateLaunchServicesPreferredVersion"
+ "msEdgeUpdateLaunchServicesPreferredVersion",
+ // gstack patch: backport of microsoft/playwright#43605 (1.65). Random client ports fail with
+ // net::ERR_NO_BUFFER_SPACE (WSAENOBUFS) on Windows even when ports are free (crbug.com/572515601).
+ "TcpPortRandomizationWin"
].filter(Boolean);
chromiumSwitches = (options) => [
"--disable-field-trial-config",
+17
View File
@@ -0,0 +1,17 @@
diff --git a/lib/coreBundle.js b/lib/coreBundle.js
index 559de664371f8abe23e261b4a1063e97727d9097..0714a29f8a82d6f085b3c3d18453bb6b7b46e5b2 100644
--- a/lib/coreBundle.js
+++ b/lib/coreBundle.js
@@ -10332,7 +10332,11 @@ async function launchProcess(options) {
env: options.env,
cwd: options.cwd,
shell: options.shell,
- windowsHide: options.windowsHide,
+ // gstack patch (#2160/#1989): Node defaults windowsHide to false for
+ // child_process.spawn, so browser children could flash a console window
+ // on Windows. Playwright never needs the child's window shown. Upstream
+ // 1.65 forwards options.windowsHide, which only the ffmpeg recorder sets.
+ windowsHide: true,
stdio
};
const spawnedProcess = childProcess.spawn(options.command, options.args || [], spawnOptions);
-2
View File
@@ -175,8 +175,6 @@ Use the picker when any of answers 1-2 is still open; it asks for them. Use dire
$B goto "<url>"
```
Tell the user once, before the import: gstack's headless browser trusts the root store built into gstack's bundled Chromium, which still accepts certificates from the compromised eMudhra intermediates. Until gstack moves to a Playwright release that drops them, don't use the imported sessions on a network the user does not trust. That same root's `BROWSER.md`, **TLS trust: the bundled Chromium's root store**, has the details and the check.
## 4. Import
Picker (the user confirms the browser, profile and domains there, ticks verification if they asked for it, then tells you when done; the link works once, for five minutes, so rerun the command if it expires):
-2
View File
@@ -51,8 +51,6 @@ Use the picker when any of answers 1-2 is still open; it asks for them. Use dire
$B goto "<url>"
```
Tell the user once, before the import: gstack's headless browser trusts the root store built into gstack's bundled Chromium, which still accepts certificates from the compromised eMudhra intermediates. Until gstack moves to a Playwright release that drops them, don't use the imported sessions on a network the user does not trust. That same root's `BROWSER.md`, **TLS trust: the bundled Chromium's root store**, has the details and the check.
## 4. Import
Picker (the user confirms the browser, profile and domains there, ticks verification if they asked for it, then tells you when done; the link works once, for five minutes, so rerun the command if it expires):