Harden Windows DLP enforcement and Electron runtime packaging

This commit is contained in:
Karl committed 2026-10-13 23:19:22 +02:00
1 parent e7d778aeac
commit 8a253eda89
120 files changed
+3569 -135

No files matched your search

+9 -1
View File
@@ -6,7 +6,15 @@ Local password vault section. The new policy defaults off and requires explicit
approved form rules. Human vault unlock remains required. No agent can unlock the
vault or export passwords through this feature.
An approved rule contains metadata only:
Use Add approved login target to edit rule ID, exact action URL and the two field
names in typed rows. Removing a row updates only the unpublished draft. Advanced
JSON remains available; invalid or unknown metadata is rejected instead of silently
discarded. Auditor controls are read-only and normal independent review remains
authoritative. An actual Windows/Electron workflow created the rule through
these four typed fields, verified independent signed publication/history restore
and auditor read-only controls. The screenshot
`artifacts/credential-submission-admin-controls.png` shows the synthetic rule;
no credential values are entered. An approved rule contains metadata only:
```json
{
+15
View File
@@ -0,0 +1,15 @@
# JavaScript lint and dependency audit
Audit date: 2026-10-13. `npm run lint` uses pinned ESLint 10.12.0, `@eslint/js` 10.0.1 and `globals` 17.13.0. Flat configuration enables recommended correctness rules, including undefined identifiers, unsafe control flow, duplicate keys and unreachable code. Node and renderer environments are separate. Serialized isolated-world DOM helpers and classic admin scripts have explicit global contracts. Narrow annotated exceptions preserve intentional control-character rejection, fixed sanitized errors, cleanup failure propagation and sensitive transient text clearing. Unused-variable warnings remain enabled, and the lint command fails on any warning. Existing unused imports and pure dead locals were removed; intentional field-stripping destructures have narrow documented annotations.
Configuration follows the official [flat-config documentation](https://eslint.org/docs/latest/use/configure/configuration-files) and [language-options documentation](https://eslint.org/docs/latest/use/configure/language-options).
`npm audit --omit=dev --json` reports zero npm advisories in production dependencies. Full `npm audit --json` reports eight moderate affected packages and zero high/critical. These eight entries form one development-tool dependency chain, rather than eight independent vulnerabilities:
`electron-builder → app-builder-lib → @electron/get → global-agent → roarr → [email protected]`.
The underlying [GHSA-hp3w-g68c-fv3c / CVE-2026-97058](https://github.com/advisories/GHSA-hp3w-g68c-fv3c) concerns denial of service when attackers control numeric precision in sprintf format strings. The advisory lists versions through 1.1.3 as affected and no patched release. The registry's latest sprintf-js is also 1.1.3. Consequently there is currently no verified patched leaf version to pin. Npm proposes electron-builder 26.5.0 despite the current pinned 26.15.3; no blind downgrade or `audit fix --force` was performed.
This is a remaining build-toolchain risk. Keep package generation limited to reviewed local source/configuration, avoid attacker-supplied format templates, and recheck the advisory before release. A production npm dependency result does not audit bundled Chromium/Electron, Python/OCR/model runtimes or prove absence of vulnerabilities. Exact Electron remains pinned at 44.7.0. Package fuse tooling is pinned separately as `@electron/[email protected]`; its package fuse enforcement requires a successful package verification result.
Raw reports remain in ignored local artifacts: `artifacts/npm-audit-production.json`, `artifacts/npm-audit-full.json`, `artifacts/eslint-final.json`. Repeat both audit commands when dependencies change; do not treat this dated snapshot as permanent evidence.
+116
View File
@@ -0,0 +1,116 @@
# Electron quality and performance review
The current Windows DLP MVP keeps security enforcement as the priority. This
review uses the official Electron documentation as the implementation authority:
[security](https://www.electronjs.org/docs/latest/tutorial/security),
[performance](https://www.electronjs.org/docs/latest/tutorial/performance),
[context isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation),
[preloads](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload),
[IPC](https://www.electronjs.org/docs/latest/tutorial/ipc),
[coding style](https://www.electronjs.org/docs/latest/development/coding-style)
and [fuses](https://www.electronjs.org/docs/latest/tutorial/fuses).
## Decisions and implementation
- Keep sandboxing, context isolation, no page Node integration, constrained
preload APIs, sender/frame validation and input validation. These need runtime
and negative tests; configuration alone does not prove a secure browser.
- Add pinned ESLint tooling and an explicit Node/browser environment contract.
Do not hide findings through blanket rule disabling. Dependency advisory
results and lint completion are tracked separately from syntax checks.
- Profile before optimizing. `scripts/profile-browser-modules.mjs` works with
Node CPU/heap profiling flags; output belongs in ignored `artifacts`.
- Defer OpenTelemetry SDK/exporter loading until diagnostics are authorized
and enabled. Keep the lightweight diagnostics API available immediately.
This shifts initialization cost to first enablement; it does not remove that
cost or claim a measured improvement to full browser startup. Seventeen focused
diagnostics regression tests pass after this change.
- Load text-normalization code on its first inspection rather than immediately
upon requiring its wrapper; subsequent calls reuse the module.
- Keep parsing, OCR and model inference in the isolated worker. Do not move
expensive inspection into the renderer or postpone mandatory enforcement.
- Review main-process synchronous filesystem/cryptographic operations using
traces before changing behavior. Build CLI `spawnSync` is separate from the
interactive browser event loop.
## Bundling and architecture
[Electron Forge](https://www.electronforge.io/) and the
[official bundling recommendation](https://www.electronjs.org/docs/latest/tutorial/performance)
are useful options. The existing electron-builder pipeline already verifies
Windows runtime contents. Migrating scaffolding is not itself a DLP improvement.
Any bundling/code-splitting change must retain sandbox-compatible preloads,
dynamic imports, packaged resources, CSP and exact package verification, and
must show measured startup or resource improvements before adoption.
The [Palette performance article](https://palette.dev/blog/improving-performance-of-electron-apps)
was inspected for additional profiling ideas. Type-safe IPC examples are useful
for maintainability; TypeScript types cannot replace runtime validation of
untrusted messages. No new IPC framework is needed merely to implement that rule.
## Source freshness
The dependency audit reports zero production advisories and eight moderate
development/build-toolchain entries, with no high or critical entries at this
review point. The underlying `sprintf-js` precision-format denial-of-service
advisory has no patched release reported; suggested electron-builder downgrade
is not accepted as a safe remediation. These findings remain explicit tooling
risk, not evidence that the deployed browser is vulnerability-free.
The packaged executable's existing fuse wire was inspected separately from
JavaScript startup checks. Run-as-Node, Node environment/CLI inspection options
and ASAR enforcement need package-level hardening and rebuilt-artifact proof.
All six hardened fuse values and the matching embedded ASAR header hash were
verified in a rebuilt executable. Actual RUN_AS_NODE and NODE_OPTIONS negative
launches passed. The CLI-inspector fixture found a startup error dialog instead
of process termination; the source now exits with a sanitized rejection, but
the corrected rebuilt package subsequently passed actual negative launches,
including eleven certificate/feature override cases and copied-ASAR tampering.
The manipulated archive was rejected before application entry; original archive
and frozen packaged sources remained unchanged. Evidence:
`artifacts/package-fuses-proof.json`. Actual signing status remains `NotSigned`.
The supplied Reasonably Secure Electron URL contained a typographic dash.
Its correct repository is
[moloch--/reasonably-secure-electron](https://github.com/moloch--/reasonably-secure-electron),
archived in June 2024; treat it as historical architecture context rather than
current Electron defaults. The supplied dev.to performance page could not be
retrieved and was not used as implementation evidence. Other supplied articles
were inspected, but version-specific changes must be checked against official
documentation and the actual installed runtime.
This review is in progress. Green syntax/tests and one profiling run do not prove
that the whole application is clean, fully optimized or independently audited.
## Latest reproducible checks
- Full source suite: 749 tests passed, no failures or skips; see the ignored
`artifacts/mvp-final-startup-suite.log`. `npm run lint` passed again with
zero warnings on 2026-10-13.
- A fresh `npm audit --omit=dev --json` reported zero advisories. Development
toolchain findings remain tracked separately in `dependency-audit.md`.
- In the paired single module-load profiles, diagnostics require time changed
from 260.119 ms to 67.001 ms after deferred SDK loading. Recorded heap deltas
changed from 6,674,088 to 2,460,832 bytes. Both used CPU/heap profiling; these
measurements do not establish full application startup or percentile gains.
- Native Windows DLP fixtures delivered public data and blocked confidential
literal and semantic payloads. Restart reused the protected device profile.
These are separate from final packaged-artifact acceptance.
- The protected local OpenTelemetry receiver reported zero rejected writes.
Trace `33b46998b44c3179cccd2826d5790c00` correlates browser.request,
browser.inspect.local, inspector.inspect, semantic load/inference and durable
audit stages. The public-file request took 8966 ms in this synthetic run;
a contained local text scan took 1611 ms and model load 662 ms. This identifies
repeated model initialization as a measurement target, not permission to
weaken process isolation or mandatory scanning.
- A native WebRTC initial-about:blank renderer crash remains reproducible under
Chromium's connection allowlist. The upstream no-op binder fix is recorded
in [the upstream regression tests](https://github.com/web-platform-tests/wpt/pull/63420).
The separately tested Electron nightly also lacks it. The Windows MVP is not
declared stable until an actual fixed runtime passes this negative fixture.
- The full enrolled browser WebTransport receiver fixture now passes through
reviewed DIRECT and fetched HTTPS PAC policies: zero managed packets/bytes,
paired against 132 baseline bytes, with ordinary HTTPS submissions working.
It records source hashes and genuine operational traces. There is still no
per-attempt WebTransport security event because this path produces no
Electron webRequest callback; transport denial and audit coverage are separate.
+108
View File
@@ -0,0 +1,108 @@
# Electron security review — 2026-10-13
This is a read-only review of the current source and unpacked Windows executable. It is not a penetration test or a claim of complete Chromium DLP coverage.
## Official references
Electron recommends isolated, sandboxed remote renderers, constrained IPC and navigation, current dependencies, and consideration of custom protocols and packaging fuses. [Security](https://www.electronjs.org/docs/latest/tutorial/security).
A bridge should expose specific actions, rather than raw IPC dispatchers. [Context isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation), [Preload scripts](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload), [IPC](https://www.electronjs.org/docs/latest/tutorial/ipc).
The subframe option is documented as experimental; sandboxing disables the renderer Node engine. [WebPreferences](https://www.electronjs.org/docs/latest/api/structures/web-preferences).
Fuses can disable alternate startup behavior and enforce ASAR loading/integrity; cookie encryption is another available control. [Fuses](https://www.electronjs.org/docs/latest/tutorial/fuses).
## Implemented boundaries inspected
| Boundary | Current evidence | Assessment |
| --- | --- | --- |
| Remote renderer | main.cjs creates WebContentsView with nodeIntegration:false, contextIsolation:true, sandbox:true, webSecurity:true, allowRunningInsecureContent:false; app.enableSandbox() runs before renderer creation | Correct explicit baseline |
| Website preload | site-preload.cjs asserts sandboxed/contextIsolated, exports no bridge; isolated fixed guards only | No raw IPC exposed to page |
| Trusted UI bridge | preload.cjs exposes fixed channel methods; onState strips event argument and supplies unsubscribe | Matches constrained bridge pattern; trusted chrome still has powerful actions |
| Privileged IPC sender | main.cjs ipc helper requires exact window WebContents, main frame and packaged UI URL | Remote and child frames excluded from trusted UI operations |
| Website reporting IPC | visual-check and page-content bind managed tab and main frame; visual reports validate bounded token/revision/URL | Separate limited channels, not generic dispatch |
| Permissions and certificates | browserSession request/check handlers deny; certificate-error callback denies | Default deny |
| Navigation/windows | trusted chrome cannot navigate/open children; managed sites checked by destination policy and agent authority, popups routed to guarded native tabs | Policy-aware handling |
| UI CSP | index.html self scripts/styles, connect-src none, object/base/form restrictions | Restrictive packaged UI CSP |
| Startup | startup-security.cjs rejects sandbox/security/isolation bypass and external debugger switches; tests/startup-security.test.mjs | Useful after-entry guard, cannot replace binary fuses |
## Experimental subframe setting
nodeIntegrationInSubFrames:true currently ensures the security preload reaches iframes. It is not evidence that page Node access is enabled: scripts/electron-smoke.cjs explicitly checks srcdoc child require/process/safetynet are undefined and sandbox remains true. scripts/native-tabs-smoke.cjs checks require and bridge absence for active, inactive and replacement tabs. site-preload.cjs additionally fails closed if isolation or sandbox changes.
Do not remove this setting solely because its name contains Node. A replacement must retain srcdoc, initial about:blank, cross-origin/OOPIF clipboard protection and the real native negative tests. The setting remains experimental and increases maintenance risk; test it on every Electron upgrade. Current assertions are not a proof against a framework sandbox vulnerability.
## Historical baseline findings
The following findings describe the initial artifact. Later package/startup
follow-ups below supersede the fuse and launch findings; the custom UI scheme,
native RTC stability and production distribution gates remain open.
1. **Historical binary fuse gap, superseded by rebuilt readback below.** An earlier direct read of dist/win-unpacked/SafetyNet.exe found V1 fuse wire 101100011. That older executable enabled RunAsNode, NODE_OPTIONS and CLI inspector and disabled cookie encryption, ASAR integrity and ASAR-only loading. This is historical evidence, not the current fuse status. Final runtime negative probes remain necessary.
2. **Trusted UI uses file://.** main.cjs builds uiUrl with pathToFileURL and calls loadFile. A dedicated standard secure custom scheme with a strict packaged asset allowlist would reduce file-origin privilege. Migration must update exact trusted-sender URL checks and every native smoke harness; do not enable generic file-system routing or bypassCSP.
3. **Subframe test breadth.** The inspected native assertion explicitly proves a same-origin srcdoc frame. Extend capability assertions to real cross-origin/OOPIF, initial blank and nested frames alongside existing clipboard bypass tests. No broad assertion about all frame configurations is justified by one fixture.
4. **Production distribution.** Source pinning and verification do not provide code signing, protected install ACLs or signed updates. External production trust requirements remain open.
## Proposed packaging patch and compatibility
After electron-builder finishes creating win-unpacked, use an explicitly pinned @electron/fuses dependency to flip RunAsNode=false, EnableNodeOptionsEnvironmentVariable=false, EnableNodeCliInspectArguments=false, EnableCookieEncryption=true, OnlyLoadAppFromAsar=true and EnableEmbeddedAsarIntegrityValidation=true on the produced executable. Confirm builder embedded the matching ASAR integrity metadata before enabling integrity; otherwise packaging must fail rather than weaken the setting. Record/read back all fuse values in verify-package.mjs.
Current verify-package.mjs launches SafetyNet.exe with --agent-action and --agent-connection through normal main.cjs and app.whenReady(), not ELECTRON_RUN_AS_NODE. Inspector/inference helpers are external Python/native binaries. No child_process.fork dependency was found in inspected browser/package paths. Thus disabling Node-mode and inspector startup is compatible with the documented current architecture, but only a rebuilt artifact and real packaged verification can prove it.
Add finite negative launch checks for RUN_AS_NODE, NODE_OPTIONS preload injection and inspector flags, plus an isolated copied-ASAR tamper test. A JS source hash comparison alone does not verify runtime archive integrity enforcement. Cookie encryption changes require a migration/reopen fixture for persistent managed sessions.
## Implementation follow-up (verification pending)
scripts/package.mjs now supplies all six hardened fuse settings through electron-builder's supported electronFuses configuration. This runs against the copied executable before signing. scripts/package-fuses.mjs validates the resulting fuse wire and parses the Windows PE INTEGRITY/ELECTRONASAR resource against the exact archive header hash. The installed Electron distribution is not modified.
Two focused unit checks passed. scripts/verify-package-fuses.mjs supplies finite actual-process negative checks and a copied archive tamper fixture; it must pass on the rebuilt artifact before this gap is considered closed. The previously measured executable remains unhardened until packaging runs. The custom-scheme gap remains open.
## Rebuilt artifact verification
The coordinated Windows build completed with all six hardened fuses and a matching embedded ASAR header hash. The standard packaged scanner/runtime/agent verifier passed. Actual negative launches prove RUN_AS_NODE cannot change the normal application entry point and a NODE_OPTIONS require injection does not execute.
The earlier CLI-inspector negative launch exposed an operational defect: Electron's default uncaught-exception handler opened an error box without terminating. The verifier correctly failed on this timeout. Current main.cjs catches startup rejection, emits a fixed message and calls process.exit(1). Final rebuilt negative launch and copied-ASAR tamper probes remain running/pending; absence of an inspector listener alone does not prove terminal behavior.
## Independent final source review
A fresh `npm run lint` passed with zero warnings allowed. `artifacts/mvp-latest-integrated-suite.log` records 748 passed tests and zero failures. These are bounded scenario evidence, not proof of every Chromium transport path. This review changed documentation only.
Current source proves explicit global sandboxing, context isolation and disabled Node integration for remote views; fixed UI bridge methods without raw IPC/event exposure; exact trusted WebContents/main-frame/URL sender checks; denied remote permissions; constrained navigation and popup handling. These match the primary [Security](https://www.electronjs.org/docs/latest/tutorial/security), [Context Isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) and [IPC](https://www.electronjs.org/docs/latest/tutorial/ipc) guidance. The experimental subframe option and file-origin UI retain the limitations stated above.
Current rebuilt fuse readback was independently verified with `verifyPackagedFuses`: RunAsNode=false, CookieEncryption=true, NodeOptions=false, NodeCliInspect=false, EmbeddedAsarIntegrity=true and OnlyLoadAppFromAsar=true. The Windows embedded archive-header hash matched. This supersedes the historical fuse wire; final negative launch/tamper probes on the final source-matched package remain pending.
`artifacts/webtransport-native-proof.json` establishes actual page and dedicated-worker delivery to a reachable pinned local HTTP/3 server in the baseline: 132 bytes, including 66 worker bytes, with zero webRequest callbacks. Neither tested disable flag stopped transmission. The authenticated production relay module blocked all receiver packets/bytes while ordinary HTTP remained usable. Main now installs the non-DIRECT relay before tab creation and restores it on policy application. This proves the standalone Session/module boundary; a full enrolled main.cjs page-and-worker receiver test across policy application remains necessary. Chromium's [dedicated WebTransport client](https://raw.githubusercontent.com/chromium/chromium/main/net/quic/dedicated_web_transport_http3_client.cc) rejects a resolved non-DIRECT proxy before socket creation. Revalidate after every Electron upgrade; no claim is made about future HTTP/2 WebTransport implementations.
**Critical unresolved stability gate:** the initial-about:blank native RTC fixture records exit code 3. It must not be presented as completed or stable protection. Source guards and passing unit tests do not close this crash or prove every nested/cross-origin context.
**Startup trust follow-up:** the reviewed denylist accepted certificate-ignore switches and user feature overrides. Root now rejects certificate-ignore/SPKI-ignore, insecure localhost/origin and reduced-security switches. Original application argv rejects user enable/disable-feature and experimental Blink overrides; Electron's internal default disable-features list is preserved. Seven focused startup tests and the updated 749-test suite passed. Actual packaged negative launches for these additional switches are pending a fresh build. The native network gate is `ConnectionAllowlists` (plural), as shown in Chromium's [navigation throttle](https://raw.githubusercontent.com/chromium/chromium/main/content/browser/renderer_host/network_restrictions_navigation_throttle.cc); a singular guessed feature name is not enforcement evidence.
Inspection/inference runs outside the main process, but synchronous audit encryption, writes/fsync and some startup reads remain on it. Traces provide timings for exercised scenarios; no universal responsiveness claim is proven. See Electron's [Performance guidance](https://www.electronjs.org/docs/latest/tutorial/performance).
### Final package follow-up
The new frozen-source Windows build and both package verifiers passed after
the startup correction. Actual baseline, RUN_AS_NODE, NODE_OPTIONS, CLI
inspector, eleven certificate/feature override launches and copied-ASAR tampering
were exercised. All unsafe startup cases terminated with the fixed rejection;
tampering failed before application entry. The original archive remained
unchanged. `artifacts/package-fuses-proof.json` records
`unsafeStartupOverridesRejected=true`. This supersedes the pending package
statements above. Actual executable status remains `NotSigned`.
### Minimal remaining enrolled WebTransport harness
Implementation follow-up: the enrolled main.cjs harness now passed, recorded in
`artifacts/fullmain-webtransport-native-proof.json`. The unguarded Window/Worker
baseline delivered 132 bytes. Actual enrollment, reviewed DIRECT and fetched
HTTPS PAC→DIRECT policies each yielded zero managed receiver packets and bytes,
while three ordinary HTTPS POSTs succeeded. Actual correlated operational
traces exported with zero current/lifetime failures. Exact source hashes are
included; the later change only normalizes Windows startup arguments. No
webRequest callback or individual WebTransport audit event was invented. This
supersedes the proposed-only status below; per-attempt security auditing remains
open.
Extend the Windows DLP smoke parent to start the existing pinned local HTTP/3 receiver and pass its ephemeral URL and certificate hash to the native fixture. First prove baseline marker delivery. After actual S Browser enrollment, attempt a datagram and stream from the managed page and a real dedicated Worker. Assert receiver connection/byte deltas, then repeat after signed policy application, including enterprise DIRECT/PAC configurations. Require ordinary HTTP/HTTPS and benign prompt delivery to keep working. Record source hashes and fixed facts without credentials or prompt contents. This extension is proposed; it is not implemented or passed here.
Upstream handler evidence: [Electron browser initialization](https://github.com/electron/electron/blob/main/lib/browser/init.ts). The verifier now records a fixed stage and bounded diagnostic output before reporting failure, and waits for child termination before removing its own validated temporary directory.
+37
View File
@@ -0,0 +1,37 @@
# Windows browser transport containment
Electron's HTTP request hooks do not intercept WebTransport payloads. A native
receiver test with Electron 44.7.0 demonstrated data from both a page and a
dedicated worker bypassing those hooks, including with the tested QUIC/Blink
disable flags. The implementation therefore forces every managed browser URL
through a private non-DIRECT Chromium proxy, with implicit loopback bypass removed.
The tested Chromium HTTP/3 WebTransport client rejects that proxy route before
opening its UDP connection. Ordinary HTTP and HTTPS remain available.
`local-dlp-relay.cjs` binds numeric loopback only and authenticates Chromium with
an ephemeral capability. It forwards approved HTTP or CONNECT traffic; it is not
a replacement for the native content-DLP gates. Native hooks still inspect text,
files and headers before release. Policy replacement, quarantine and shutdown
invalidate relay authority and terminate pending/live sockets. Resolution uses
the approved Chromium resolver and numeric connection addresses.
The management session retains the centrally configured system/fixed/PAC proxy.
The relay resolves upstream routes through that session. DIRECT, HTTP PROXY and
HTTPS PROXY routes are supported; an unsupported or failed route never silently
falls back to DIRECT. SOCKS and integrated upstream NTLM/Kerberos authentication
currently fail closed and require an additional integration. Optional Basic
credentials require `SAFETYNET_PROXY_AUTH_HOST` and `SAFETYNET_PROXY_AUTH_PORT`
to match the selected upstream endpoint, in addition to protected deployment
username/password variables. They are not distributed in policy bundles.
Native evidence: `artifacts/webtransport-native-proof.json`. The receiver got
zero packets/bytes with the private relay while ordinary HTTP loaded. The
Windows core DLP and real restart fixtures also passed with relay integration.
Chromium upgrades must repeat this receiver proof; it is version-specific.
WebTransport-specific denial audit events are not yet observed because the
native rejection occurs before existing HTTP/relay callbacks. Do not claim
complete transport-attempt SIEM coverage from the ordinary DLP event pipeline.
Inspected WebSocket/WebRTC support remains the intended compatibility goal.
Uninspected transports remain restricted until their content gates have their
own actual receiver and bypass tests.
+107
View File
@@ -0,0 +1,107 @@
# Chromium 152 WebRTC binder backport
Status: source patch prepared and checked; **not compiled or runtime verified**.
Do not distribute this patch as a proven security fix until the acceptance gates
below pass against the resulting binary.
## Exact basis and provenance
The adjacent patch targets Chromium `152.0.7977.130`, used by Electron `44.7.0`.
It backports Chromium commit
[`46fcf3faed9db394de7046a100301f64c2842f0e`](https://chromium.googlesource.com/chromium/src/+/46fcf3faed9db394de7046a100301f64c2842f0e)
(CL8485457, bug565837691). Upstream files retain their Chromium copyright notices.
The upstream change fixes a native renderer termination when nested local-scheme
frames request a missing WebRTC binder under `Connection-Allowlist`. It registers
the binders consistently, checks the current document policy when binding, and
uses no-op mDNS/socket implementations to fail without creating network sockets.
It is a temporary transport denial, not WebRTC payload inspection.
Only two context adaptations were needed: insert the policy helper before the
older utilities file's namespace close, and place the conditional mDNS include
beside the existing connection-policy include. The original implementation,
unit tests, WPT tests and GN entries are preserved. The older public
`RenderFrameHost` header exposes both APIs used by the helper:
`HasPolicyContainerHost()` and `GetConnectionAllowlists()`.
On 2026-10-13, the 16 exact touched paths were retrieved individually from the
official Chromium tag into an ignored scratch repository. Unmodified tag files
were restored, with LF line endings, and this command completed with exit0:
```powershell
git -C artifacts/rtc-patch-check apply --check C:/Users/karl/Safetynet/docs/native-runtime/chromium-152-webrtc-binder-backport.patch
```
This proves textual applicability to the tagged files. It does not establish
compilation, compatibility with Electron's existing patches, or runtime safety.
Repeat the check against the complete Electron-patched source checkout.
## Host readiness
Measured host: Ryzen7800X3D, eight exposed logical processors, 16GB RAM (about
4.5GB free during inspection), and 266.6GiB free on NTFS C:. Installed VS2022
Build Tools17.14 includes x64 C++ tools but lacks the ATL/MFC component. SDK
`rc.exe` is26100.7705; `symstore.exe` is28000.2957. No native checkout or
depot_tools/gclient/GN/Ninja installation was found on PATH.
The [version-specific Chromium requirements](https://chromium.googlesource.com/chromium/src/+/152.0.7977.130/docs/windows_build_instructions.md)
specify at least8GB RAM and100GB disk, recommend more than16GB RAM, and require
C++ plus ATL/MFC and SDK26100.7705. This host meets the minimum resources but
requires toolchain completion. Plan substantially more memory and disk headroom
for comfortable builds; actual full-checkout and output size remains unmeasured.
Do not automatically disable endpoint antivirus or change global Git settings.
## Build procedure
Follow [Electron Windows prerequisites](https://www.electronjs.org/docs/latest/development/build-instructions-windows)
and [Electron GN/patch instructions](https://www.electronjs.org/docs/latest/development/build-instructions-gn).
Use a separate short path such as `C:/src/electron44`, outside this application.
Install missing ATL/MFC and depot_tools first. No large downloads or dependency
changes were performed while preparing this review artifact.
1. Set `DEPOT_TOOLS_WIN_TOOLCHAIN=0` in the build process and prepend depot_tools
to that process's PATH. Keep the application workspace unchanged.
2. Configure gclient with unmanaged `src/electron` pointing at the official
Electron repository. Bootstrap its source, check out exact `v44.7.0` in
`src/electron`, then sync with branch heads and tags. Confirm its DEPS pins
Chromium152.0.7977.130 before applying anything.
3. From `src`, run `git apply --check <absolute-patch-path>` against the already
Electron-patched checkout, then apply. Any failure requires review; do not
silently drop hunks. Export the change through Electron's patch system
(`e patches`) so future synchronizations reproduce it.
4. Generate a release configuration using
`import("//electron/build/args/release.gn") target_cpu="x64"`.
For this constrained host, consider `symbol_level=0`,
`blink_symbol_level=0`, `v8_symbol_level=0`, and local execution. These reduce
debugging detail; keep a separately instrumented build if native investigation
requires symbols.
5. Build `ninja -C out/Release -j2 electron`. Build the relevant Chromium native
test target as well; low parallelism is an initial resource choice, not a
measured guarantee against memory exhaustion.
6. Only after verification, package with
`ninja -C out/Release electron:electron_dist_zip` and pin artifact hash,
Electron/Chromium versions and backport revision in release provenance.
## Acceptance gates
- Compile and run `NoOpMdnsResponderTest.*` and `NoOpP2PSocketManagerTest.*`.
- Execute the upstream local-scheme allow/block WPT cases.
- Run real paired WebRTC data-channel positive controls in initial about:blank,
srcdoc, blob and cross-origin frames; prove exact synthetic bytes arrive.
- Under the actual managed response-header function, repeat the same attempts:
no data delivery, no renderer termination, no hanging harness, and successful
benign HTTP POSTs. Exercise the initial synchronous iframe window before any
navigation or preload; constructor availability alone is not a pass.
- Verify native socket denial without relying on the JavaScript preload. Check
redirects and a site-supplied `webrtc=allow` header cannot relax managed policy.
- Run the complete Windows DLP MVP native acceptance suite and inspect actual
exported OpenTelemetry traces, durable deny events and exporter failures.
## Alternative runtime status
No tested prebuilt alternative is verified. Electron46 nightly20261013 with
Chromium157.0.8091.0 also crashes under the same blocked about:blank/srcdoc
attempts; its tagged binder source lacks the no-op implementation. A later
binary is acceptable only after source inclusion checks and all native gates
above. Current ignored artifacts retain the original crashes and positive
controls; an upgrade must not overwrite that evidence.
@@ -0,0 +1,903 @@
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 7b194ea..f73edef 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -2521,8 +2521,17 @@ source_set("browser") {
deps += [ "//media/mojo/mojom" ]
}
+ if (enable_mdns) {
+ sources += [
+ "renderer_host/no_op_mdns_responder.cc",
+ "renderer_host/no_op_mdns_responder.h",
+ ]
+ }
+
if (is_p2p_enabled) {
sources += [
+ "renderer_host/p2p/no_op_p2p_socket_manager.cc",
+ "renderer_host/p2p/no_op_p2p_socket_manager.h",
"renderer_host/p2p/socket_dispatcher_host.cc",
"renderer_host/p2p/socket_dispatcher_host.h",
]
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index 94c73d9..26ca7ce 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -27,6 +27,7 @@
#include "content/browser/bluetooth/web_bluetooth_service_impl.h"
#include "content/browser/browser_context_impl.h"
#include "content/browser/browser_main_loop.h"
+#include "content/browser/connection_allowlist_utils.h"
#include "content/browser/contacts/contacts_manager_impl.h"
#include "content/browser/content_index/content_index_service_impl.h"
#include "content/browser/cookie_store/cookie_store_manager.h"
@@ -119,6 +120,7 @@
#include "services/device/public/mojom/vibration_manager.mojom.h"
#include "services/metrics/public/mojom/ukm_interface.mojom.h"
#include "services/metrics/ukm_recorder_factory_impl.h"
+#include "services/network/public/cpp/connection_allowlist.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/network_service_buildflags.h"
@@ -225,6 +227,7 @@
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_P2P_ENABLED)
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
#include "services/network/public/mojom/p2p.mojom.h"
#endif // BUILDFLAG(IS_P2P_ENABLED)
@@ -790,6 +793,17 @@ void BindMediaPlayerObserverClientHandler(
void BindSocketManager(
RenderFrameHost* frame,
mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver) {
+ // If WebRTC is blocked by the document's Connection Allowlist, bind a no-op
+ // P2PSocketManager so that no sockets are ever created, while still letting
+ // the renderer fail gracefully (see NoOpP2PSocketManager). It is possible
+ // that a renderer which does not allow WebRTC could still reach this
+ // interface. TODO(crbug.com/571239543): Disable all WebRTC connections on the
+ // renderer side.
+ if (!IsWebRTCAllowedByConnectionAllowlist(frame)) {
+ NoOpP2PSocketManager::Create(std::move(receiver));
+ return;
+ }
+
static_cast<RenderProcessHostImpl*>(frame->GetProcess())
->BindP2PSocketManager(
frame->GetIsolationInfoForSubresources().network_anonymization_key(),
@@ -991,26 +1005,9 @@ void PopulateBinderMapWithContext(
map->Add<blink::mojom::IdleManager>(
&BindRenderFrameHostImpl<&RenderFrameHostImpl::BindIdleManager>);
-#if BUILDFLAG(IS_P2P_ENABLED) || BUILDFLAG(ENABLE_MDNS)
- // TODO(447954811): Remove the fenced frames check if we end up supporting
- // Connection Allowlist for fenced frames.
- bool should_ban_p2p_for_connection_allowlist =
- host->HasPolicyContainerHost() &&
- host->policy_container_host()
- ->connection_allowlists()
- .enforced.has_value() &&
- host->policy_container_host()
- ->connection_allowlists()
- .enforced->webrtc_behavior ==
- network::ConnectionAllowlist::WebRtcBehavior::kBlock &&
- !host->IsNestedWithinFencedFrame();
-# endif // BUILDFLAG(IS_P2P_ENABLED) || BUILDFLAG(ENABLE_MDNS)
-
#if BUILDFLAG(ENABLE_MDNS)
- if (!should_ban_p2p_for_connection_allowlist) {
- map->Add<network::mojom::MdnsResponder>(
+ map->Add<network::mojom::MdnsResponder>(
&BindRenderFrameHostImpl<&RenderFrameHostImpl::CreateMdnsResponder>);
- }
#endif // BUILDFLAG(ENABLE_MDNS)
// BrowserMainLoop::GetInstance() may be null on unit tests.
@@ -1027,18 +1024,7 @@ void PopulateBinderMapWithContext(
&RenderFrameHostImpl::CreateNotificationService>);
#if BUILDFLAG(IS_P2P_ENABLED)
- // WebRTC p2p connections are disallowed in fenced frames. Creation of
- // RTCPeerConnection is already disabled in the renderer, so in theory this
- // unbound interface should never present an issue.
- bool should_ban_p2p_for_fenced_frames =
- base::FeatureList::IsEnabled(
- blink::features::kFencedFramesLocalUnpartitionedDataAccess) &&
- host->IsNestedWithinFencedFrame();
-
- if (!should_ban_p2p_for_fenced_frames &&
- !should_ban_p2p_for_connection_allowlist) {
- map->Add<network::mojom::P2PSocketManager>(&BindSocketManager);
- }
+ map->Add<network::mojom::P2PSocketManager>(&BindSocketManager);
#endif // BUILDFLAG(IS_P2P_ENABLED)
map->Add<blink::mojom::PeerConnectionTrackerHost>(
diff --git a/content/browser/connection_allowlist_utils.cc b/content/browser/connection_allowlist_utils.cc
index 379f029..b366763 100644
--- a/content/browser/connection_allowlist_utils.cc
+++ b/content/browser/connection_allowlist_utils.cc
@@ -141,4 +141,22 @@ bool FrameConnectionAllowlistAllowsRequestAndReportIfNeeded(
return ConnectionAllowlistAllowsUrlAndReportIfNeeded(policies, url);
}
+bool IsWebRTCAllowedByConnectionAllowlist(RenderFrameHost* frame) {
+ // A speculative RFH doesn't have a PolicyContainerHost, and in theory it
+ // should not be binding WebRTC interfaces while speculative. So fail closed.
+ if (!frame->HasPolicyContainerHost()) {
+ return false;
+ }
+
+ const std::optional<network::ConnectionAllowlist>& enforced_ca =
+ frame->GetConnectionAllowlists().enforced;
+ if (enforced_ca.has_value() &&
+ enforced_ca->webrtc_behavior ==
+ network::ConnectionAllowlist::WebRtcBehavior::kBlock) {
+ return false;
+ }
+
+ return true;
+}
+
} // namespace content
diff --git a/content/browser/connection_allowlist_utils.h b/content/browser/connection_allowlist_utils.h
index ee65754..ab5ecae 100644
--- a/content/browser/connection_allowlist_utils.h
+++ b/content/browser/connection_allowlist_utils.h
@@ -15,6 +15,7 @@ class HttpResponseHeaders;
namespace content {
+class RenderFrameHost;
struct PolicyContainerPolicies;
// Returns true if the parsed response headers contains a valid
@@ -47,6 +48,12 @@ network::ConnectionAllowlists GetConnectionAllowlistsForWorker(
const PolicyContainerPolicies* creator_policies,
bool inherit_from_creator);
+// Determines if the Connection Allowlist for `frame` has allowed WebRTC. This
+// is controlled by the Connection Allowlist header's `webrtc` directive, and is
+// is used to prevent binding of downstream interfaces like P2PSocketManger and
+// MDNSResponder.
+bool IsWebRTCAllowedByConnectionAllowlist(RenderFrameHost* frame);
+
} // namespace content
#endif // CONTENT_BROWSER_CONNECTION_ALLOWLIST_UTILS_H_
diff --git a/content/browser/renderer_host/no_op_mdns_responder.cc b/content/browser/renderer_host/no_op_mdns_responder.cc
new file mode 100644
index 0000000..d2ff53d
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder.cc
@@ -0,0 +1,38 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+
+namespace content {
+
+// static
+void NoOpMdnsResponder::Create(
+ mojo::PendingReceiver<network::mojom::MdnsResponder> receiver) {
+ mojo::MakeSelfOwnedReceiver(std::make_unique<NoOpMdnsResponder>(),
+ std::move(receiver));
+}
+
+NoOpMdnsResponder::NoOpMdnsResponder() = default;
+NoOpMdnsResponder::~NoOpMdnsResponder() = default;
+
+void NoOpMdnsResponder::CreateNameForAddress(
+ const net::IPAddress& address,
+ CreateNameForAddressCallback callback) {
+ std::move(callback).Run(/*name=*/std::string(),
+ /*announcement_scheduled=*/false);
+}
+
+void NoOpMdnsResponder::RemoveNameForAddress(
+ const net::IPAddress& address,
+ RemoveNameForAddressCallback callback) {
+ std::move(callback).Run(/*removed=*/false, /*goodbye_scheduled=*/false);
+}
+
+} // namespace content
diff --git a/content/browser/renderer_host/no_op_mdns_responder.h b/content/browser/renderer_host/no_op_mdns_responder.h
new file mode 100644
index 0000000..3f6556b
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder.h
@@ -0,0 +1,39 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
+#define CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
+
+#include "content/common/content_export.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/mdns_responder.mojom.h"
+
+namespace content {
+
+// An MdnsResponder that never registers or announces any names. It is bound in
+// place of the real (network service backed) MdnsResponder for documents whose
+// Connection Allowlist blocks WebRTC. All reply callbacks are run with failure
+// values so that no renderer request hangs.
+class CONTENT_EXPORT NoOpMdnsResponder : public network::mojom::MdnsResponder {
+ public:
+ // Creates a self-owned NoOpMdnsResponder bound to `receiver`. It is destroyed
+ // when the renderer closes the pipe.
+ static void Create(
+ mojo::PendingReceiver<network::mojom::MdnsResponder> receiver);
+
+ NoOpMdnsResponder();
+ NoOpMdnsResponder(const NoOpMdnsResponder&) = delete;
+ NoOpMdnsResponder& operator=(const NoOpMdnsResponder&) = delete;
+ ~NoOpMdnsResponder() override;
+
+ // network::mojom::MdnsResponder:
+ void CreateNameForAddress(const net::IPAddress& address,
+ CreateNameForAddressCallback callback) override;
+ void RemoveNameForAddress(const net::IPAddress& address,
+ RemoveNameForAddressCallback callback) override;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
diff --git a/content/browser/renderer_host/no_op_mdns_responder_unittest.cc b/content/browser/renderer_host/no_op_mdns_responder_unittest.cc
new file mode 100644
index 0000000..0a256ad
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder_unittest.cc
@@ -0,0 +1,87 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+
+#include <string>
+
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/ip_address.h"
+#include "services/network/public/mojom/mdns_responder.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+class NoOpMdnsResponderTest : public testing::Test {
+ public:
+ NoOpMdnsResponderTest() {
+ NoOpMdnsResponder::Create(responder_.BindNewPipeAndPassReceiver());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ mojo::Remote<network::mojom::MdnsResponder> responder_;
+};
+
+// CreateNameForAddress() must always run its callback, without creating a name
+// or scheduling an announcement.
+TEST_F(NoOpMdnsResponderTest, CreateNameForAddressFails) {
+ base::test::TestFuture<const std::string&, bool> future;
+ responder_->CreateNameForAddress(net::IPAddress(192, 168, 0, 1),
+ future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_FALSE(future.Get<1>());
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// Repeated requests for the same address also fail; no name is ever retained.
+TEST_F(NoOpMdnsResponderTest, CreateNameForAddressRepeatedlyFails) {
+ const net::IPAddress address(10, 0, 0, 1);
+ for (int i = 0; i < 3; ++i) {
+ base::test::TestFuture<const std::string&, bool> future;
+ responder_->CreateNameForAddress(address, future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_FALSE(future.Get<1>());
+ }
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// RemoveNameForAddress() must always run its callback, reporting that nothing
+// was removed and no goodbye was scheduled.
+TEST_F(NoOpMdnsResponderTest, RemoveNameForAddressFails) {
+ base::test::TestFuture<bool, bool> future;
+ responder_->RemoveNameForAddress(net::IPAddress(192, 168, 0, 1),
+ future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_FALSE(future.Get<0>());
+ EXPECT_FALSE(future.Get<1>());
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// Works for IPv6 addresses too.
+TEST_F(NoOpMdnsResponderTest, Ipv6AddressFails) {
+ base::test::TestFuture<const std::string&, bool> create_future;
+ responder_->CreateNameForAddress(net::IPAddress::IPv6Localhost(),
+ create_future.GetCallback());
+ ASSERT_TRUE(create_future.Wait());
+ EXPECT_TRUE(create_future.Get<0>().empty());
+ EXPECT_FALSE(create_future.Get<1>());
+
+ base::test::TestFuture<bool, bool> remove_future;
+ responder_->RemoveNameForAddress(net::IPAddress::IPv6Localhost(),
+ remove_future.GetCallback());
+ ASSERT_TRUE(remove_future.Wait());
+ EXPECT_FALSE(remove_future.Get<0>());
+ EXPECT_FALSE(remove_future.Get<1>());
+}
+
+} // namespace
+
+} // namespace content
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc
new file mode 100644
index 0000000..b00ee09
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
+
+#include <memory>
+#include <utility>
+
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "net/base/ip_address.h"
+
+namespace content {
+
+// static
+void NoOpP2PSocketManager::Create(
+ mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver) {
+ mojo::MakeSelfOwnedReceiver(std::make_unique<NoOpP2PSocketManager>(),
+ std::move(receiver));
+}
+
+NoOpP2PSocketManager::NoOpP2PSocketManager() = default;
+NoOpP2PSocketManager::~NoOpP2PSocketManager() = default;
+
+void NoOpP2PSocketManager::StartNetworkNotifications(
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient> client) {
+ network_notification_client_.reset();
+ network_notification_client_.Bind(std::move(client));
+ // Report an empty network list so nothing is waiting on network changes.
+ network_notification_client_->NetworkListChanged(
+ /*networks=*/{}, /*default_ipv4_local_address=*/net::IPAddress(),
+ /*default_ipv6_local_address=*/net::IPAddress());
+}
+
+void NoOpP2PSocketManager::GetHostAddress(
+ const std::string& host_name,
+ std::optional<net::AddressFamily> address_family,
+ bool enable_mdns,
+ GetHostAddressCallback callback) {
+ // Never resolve anything.
+ std::move(callback).Run(/*addresses=*/{});
+}
+
+void NoOpP2PSocketManager::CreateSocket(
+ network::P2PSocketType type,
+ const net::IPEndPoint& local_address,
+ const network::P2PPortRange& port_range,
+ const network::P2PHostAndIPEndPoint& remote_address,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
+ const std::optional<base::UnguessableToken>& devtools_token,
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client,
+ mojo::PendingReceiver<network::mojom::P2PSocket> receiver) {
+ // Never create a socket. Letting these pending pipes go out of scope should
+ // destroy them and treat them as disconnected.
+}
+
+} // namespace content
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h
new file mode 100644
index 0000000..cf05b8f
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h
@@ -0,0 +1,69 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
+#define CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
+
+#include <optional>
+#include <string>
+
+#include "content/common/content_export.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/network/public/mojom/p2p.mojom.h"
+
+namespace content {
+
+// A P2PSocketManager that never performs any networking. It is bound in place
+// of the real (network service backed) P2PSocketManager for documents whose
+// Connection Allowlist blocks WebRTC.
+//
+// Binding a no-op implementation (rather than dropping the receiver or leaving
+// the interface unregistered) lets an unmodified renderer fail gracefully:
+// - The pipe stays connected, so the renderer's P2PSocketDispatcher does not
+// enter its reconnect loop.
+// - An empty network list is reported, so ICE gathering is allowed to proceed.
+// - All reply callbacks are run, so no request hangs.
+class CONTENT_EXPORT NoOpP2PSocketManager
+ : public network::mojom::P2PSocketManager {
+ public:
+ // Creates a self-owned NoOpP2PSocketManager bound to `receiver`. It is
+ // destroyed when the renderer closes the pipe.
+ static void Create(
+ mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver);
+
+ NoOpP2PSocketManager();
+ NoOpP2PSocketManager(const NoOpP2PSocketManager&) = delete;
+ NoOpP2PSocketManager& operator=(const NoOpP2PSocketManager&) = delete;
+ ~NoOpP2PSocketManager() override;
+
+ // network::mojom::P2PSocketManager:
+ void StartNetworkNotifications(
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient> client)
+ override;
+ void GetHostAddress(const std::string& host_name,
+ std::optional<net::AddressFamily> address_family,
+ bool enable_mdns,
+ GetHostAddressCallback callback) override;
+ void CreateSocket(
+ network::P2PSocketType type,
+ const net::IPEndPoint& local_address,
+ const network::P2PPortRange& port_range,
+ const network::P2PHostAndIPEndPoint& remote_address,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
+ const std::optional<base::UnguessableToken>& devtools_token,
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client,
+ mojo::PendingReceiver<network::mojom::P2PSocket> receiver) override;
+
+ private:
+ // Kept alive for the lifetime of this object so that the renderer does not
+ // observe a disconnect on its notification client.
+ mojo::Remote<network::mojom::P2PNetworkNotificationClient>
+ network_notification_client_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc
new file mode 100644
index 0000000..c83b26e
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc
@@ -0,0 +1,171 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
+
+#include <optional>
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/network_interfaces.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/p2p_socket_type.h"
+#include "services/network/public/mojom/p2p.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+class FakeNetworkNotificationClient
+ : public network::mojom::P2PNetworkNotificationClient {
+ public:
+ FakeNetworkNotificationClient() = default;
+ FakeNetworkNotificationClient(const FakeNetworkNotificationClient&) = delete;
+ FakeNetworkNotificationClient& operator=(
+ const FakeNetworkNotificationClient&) = delete;
+ ~FakeNetworkNotificationClient() override = default;
+
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient>
+ BindNewPipeAndPassRemote() {
+ auto remote = receiver_.BindNewPipeAndPassRemote();
+ receiver_.set_disconnect_handler(base::BindOnce(
+ [](bool* disconnected) { *disconnected = true; }, &disconnected_));
+ return remote;
+ }
+
+ bool disconnected() const { return disconnected_; }
+
+ base::test::TestFuture<std::vector<net::NetworkInterface>,
+ net::IPAddress,
+ net::IPAddress>&
+ network_list_future() {
+ return network_list_future_;
+ }
+
+ // network::mojom::P2PNetworkNotificationClient:
+ void NetworkListChanged(
+ const std::vector<net::NetworkInterface>& networks,
+ const net::IPAddress& default_ipv4_local_address,
+ const net::IPAddress& default_ipv6_local_address) override {
+ network_list_future_.SetValue(networks, default_ipv4_local_address,
+ default_ipv6_local_address);
+ }
+
+ private:
+ mojo::Receiver<network::mojom::P2PNetworkNotificationClient> receiver_{this};
+ base::test::TestFuture<std::vector<net::NetworkInterface>,
+ net::IPAddress,
+ net::IPAddress>
+ network_list_future_;
+ bool disconnected_ = false;
+};
+
+class NoOpP2PSocketManagerTest : public testing::Test {
+ public:
+ NoOpP2PSocketManagerTest() {
+ NoOpP2PSocketManager::Create(manager_.BindNewPipeAndPassReceiver());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ mojo::Remote<network::mojom::P2PSocketManager> manager_;
+};
+
+// StartNetworkNotifications() must report an (empty) network list so that the
+// renderer does not wait forever for one, and must keep the client connected.
+TEST_F(NoOpP2PSocketManagerTest, StartNetworkNotificationsReportsEmptyList) {
+ FakeNetworkNotificationClient client;
+ manager_->StartNetworkNotifications(client.BindNewPipeAndPassRemote());
+
+ auto& future = client.network_list_future();
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_TRUE(future.Get<1>().empty());
+ EXPECT_TRUE(future.Get<2>().empty());
+
+ // The notification client must remain connected; otherwise the renderer
+ // would observe a disconnect.
+ manager_.FlushForTesting();
+ EXPECT_FALSE(client.disconnected());
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// A second StartNetworkNotifications() call replaces the previous client and
+// also receives an empty network list.
+TEST_F(NoOpP2PSocketManagerTest, StartNetworkNotificationsTwice) {
+ FakeNetworkNotificationClient first_client;
+ manager_->StartNetworkNotifications(first_client.BindNewPipeAndPassRemote());
+ ASSERT_TRUE(first_client.network_list_future().Wait());
+
+ FakeNetworkNotificationClient second_client;
+ manager_->StartNetworkNotifications(second_client.BindNewPipeAndPassRemote());
+ auto& future = second_client.network_list_future();
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+
+ manager_.FlushForTesting();
+ EXPECT_TRUE(first_client.disconnected());
+ EXPECT_FALSE(second_client.disconnected());
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// GetHostAddress() must always run its callback, with no addresses.
+TEST_F(NoOpP2PSocketManagerTest, GetHostAddressReturnsNoAddresses) {
+ base::test::TestFuture<const std::vector<net::IPAddress>&> future;
+ manager_->GetHostAddress("example.com", /*address_family=*/std::nullopt,
+ /*enable_mdns=*/true, future.GetCallback());
+ EXPECT_TRUE(future.Get().empty());
+
+ // mDNS-style names must not be resolved either.
+ base::test::TestFuture<const std::vector<net::IPAddress>&> mdns_future;
+ manager_->GetHostAddress("00000000-0000-0000-0000-000000000000.local",
+ /*address_family=*/std::nullopt,
+ /*enable_mdns=*/true, mdns_future.GetCallback());
+ EXPECT_TRUE(mdns_future.Get().empty());
+
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// CreateSocket() must never create a socket. The socket pipe is closed, so the
+// renderer treats the socket as failed, while the manager stays connected (so
+// the renderer does not attempt to reconnect).
+TEST_F(NoOpP2PSocketManagerTest, CreateSocketClosesSocketPipe) {
+ mojo::PendingReceiver<network::mojom::P2PSocketClient> client_receiver;
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client =
+ client_receiver.InitWithNewPipeAndPassRemote();
+
+ mojo::Remote<network::mojom::P2PSocket> socket;
+ base::test::TestFuture<void> socket_disconnected;
+ auto socket_receiver = socket.BindNewPipeAndPassReceiver();
+ socket.set_disconnect_handler(socket_disconnected.GetCallback());
+
+ manager_->CreateSocket(
+ network::P2P_SOCKET_UDP,
+ net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0),
+ network::P2PPortRange(),
+ network::P2PHostAndIPEndPoint(
+ "example.com", net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0)),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
+ /*devtools_token=*/std::nullopt, std::move(client),
+ std::move(socket_receiver));
+
+ EXPECT_TRUE(socket_disconnected.Wait());
+ EXPECT_FALSE(socket.is_connected());
+
+ manager_.FlushForTesting();
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+} // namespace
+
+} // namespace content
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index a08ebce..2829cbb 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -88,6 +88,10 @@
#include "content/browser/can_commit_status.h"
#include "content/browser/closewatcher/close_listener_host.h"
#include "content/browser/code_cache/generated_code_cache_context.h"
+#include "content/browser/connection_allowlist_utils.h"
+#if BUILDFLAG(ENABLE_MDNS)
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+#endif
#include "content/browser/data_url_loader_factory.h"
#include "content/browser/declarative_performance_observer/declarative_performance_observer.h"
#include "content/browser/devtools/devtools_instrumentation.h"
@@ -19515,6 +19519,17 @@ RenderFrameHostImpl::CreateDeviceBoundSessionObserver() {
#if BUILDFLAG(ENABLE_MDNS)
void RenderFrameHostImpl::CreateMdnsResponder(
mojo::PendingReceiver<network::mojom::MdnsResponder> receiver) {
+ // If WebRTC is blocked by this document's Connection Allowlist, bind a no-op
+ // MdnsResponder so that no names are ever registered or announced, while
+ // still replying to every renderer request (see NoOpMdnsResponder).
+ // It is possible that a renderer which does not allow WebRTC could still
+ // reach this interface. TODO(crbug.com/571239543): Disable all WebRTC
+ // connections on the renderer side.
+ if (!IsWebRTCAllowedByConnectionAllowlist(this)) {
+ NoOpMdnsResponder::Create(std::move(receiver));
+ return;
+ }
+
GetStoragePartition()->GetNetworkContext()->CreateMdnsResponder(
std::move(receiver));
}
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 7ecab4f..454e0aa 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -21,6 +21,7 @@ import("//mojo/public/tools/bindings/mojom.gni")
import("//net/features.gni")
import("//printing/buildflags/buildflags.gni")
import("//services/device/public/cpp/compute_pressure/buildflags.gni")
+import("//services/network/public/cpp/features.gni")
import("//services/screen_ai/buildflags/features.gni")
import("//testing/test.gni")
import("//third_party/blink/public/public_features.gni")
@@ -3402,6 +3403,16 @@ test("content_unittests") {
deps += [ "//content/browser/smart_card/emulation" ]
}
+ if (enable_mdns) {
+ sources += [ "../browser/renderer_host/no_op_mdns_responder_unittest.cc" ]
+ }
+
+ if (is_p2p_enabled) {
+ sources +=
+ [ "../browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc" ]
+ deps += [ "//services/network/public/cpp:cpp_p2p" ]
+ }
+
if (enable_nocompile_tests) {
deps += [ ":content_nocompile_tests" ]
}
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js
new file mode 100644
index 0000000..cce0fd7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js
@@ -0,0 +1,74 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// The following tests assume the policy `Connection-Allowlist:
+// (response-origin);webrtc=allow` has been set. Child frames with a local
+// scheme (about:blank, about:srcdoc) inherit the parent's policy container,
+// and should continue to allow WebRTC, including when nested.
+
+const WEBRTC_HELPER = '/content-security-policy/webrtc/webrtc.js';
+// Loads webrtc.js into `win`'s own document, so `tryConnect` (and the
+// RTCPeerConnection it uses) run in the child's realm, not the parent's.
+function loadHelper(win) {
+ return new Promise((resolve, reject) => {
+ const script = win.document.createElement('script');
+ script.src = WEBRTC_HELPER;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error(`Failed to load ${WEBRTC_HELPER}`));
+ win.document.head.appendChild(script);
+ });
+}
+
+async function tryConnectIn(win) {
+ await loadHelper(win);
+ return win.tryConnect();
+}
+
+// Appends an iframe with no `src` to `doc`. The frame stays on its initial
+// about:blank document, which is usable synchronously.
+function addBlankFrame(t, doc) {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ doc.body.appendChild(frame);
+ return frame;
+}
+
+// Appends an about:srcdoc iframe to `doc`, and waits for the srcdoc document
+// to load (before that, `contentWindow` is still the initial about:blank
+// document).
+async function addSrcdocFrame(
+ t, doc, srcdoc = '<!doctype html><body><p>Hello World</p></body>') {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ const loaded = new Promise(resolve => frame.onload = resolve);
+ frame.srcdoc = srcdoc;
+ doc.body.appendChild(frame);
+ await loaded;
+ return frame;
+}
+
+promise_test(async t => {
+ assert_equals(await tryConnect(), 'allowed');
+}, 'WebRTC is allowed in the top-level document.');
+
+promise_test(async t => {
+ const frame = addBlankFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'allowed');
+}, 'WebRTC is allowed in an about:blank child frame.');
+
+promise_test(async t => {
+ const frame = await addSrcdocFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'allowed');
+}, 'WebRTC is allowed in an about:srcdoc child frame.');
+
+promise_test(async t => {
+ const outer = addBlankFrame(t, document);
+ const inner = addBlankFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'allowed');
+}, 'WebRTC is allowed in a nested about:blank frame.');
+
+promise_test(async t => {
+ const outer = await addSrcdocFrame(t, document);
+ const inner = await addSrcdocFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'allowed');
+}, 'WebRTC is allowed in a nested about:srcdoc frame.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers
new file mode 100644
index 0000000..e8ebc16
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers
@@ -0,0 +1 @@
+Connection-Allowlist: (response-origin);webrtc=allow
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js
new file mode 100644
index 0000000..e782a57
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js
@@ -0,0 +1,75 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// The following tests assume the policy `Connection-Allowlist:
+// (response-origin)` has been set. WebRTC has been blocked by default. Child
+// frames with a local scheme (about:blank, about:srcdoc) inherit the parent's
+// policy container, and should continue to block WebRTC, including when
+// nested.
+
+const WEBRTC_HELPER = '/content-security-policy/webrtc/webrtc.js';
+// Loads webrtc.js into `win`'s own document, so `tryConnect` (and the
+// RTCPeerConnection it uses) run in the child's realm, not the parent's.
+function loadHelper(win) {
+ return new Promise((resolve, reject) => {
+ const script = win.document.createElement('script');
+ script.src = WEBRTC_HELPER;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error(`Failed to load ${WEBRTC_HELPER}`));
+ win.document.head.appendChild(script);
+ });
+}
+
+async function tryConnectIn(win) {
+ await loadHelper(win);
+ return win.tryConnect();
+}
+
+// Appends an iframe with no `src` to `doc`. The frame stays on its initial
+// about:blank document, which is usable synchronously.
+function addBlankFrame(t, doc) {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ doc.body.appendChild(frame);
+ return frame;
+}
+
+// Appends an about:srcdoc iframe to `doc`, and waits for the srcdoc document
+// to load (before that, `contentWindow` is still the initial about:blank
+// document).
+async function addSrcdocFrame(
+ t, doc, srcdoc = '<!doctype html><body><p>Hello World</p></body>') {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ const loaded = new Promise(resolve => frame.onload = resolve);
+ frame.srcdoc = srcdoc;
+ doc.body.appendChild(frame);
+ await loaded;
+ return frame;
+}
+
+promise_test(async t => {
+ assert_equals(await tryConnect(), 'blocked');
+}, 'WebRTC is blocked in the top-level document.');
+
+promise_test(async t => {
+ const frame = addBlankFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'blocked');
+}, 'WebRTC is blocked in an about:blank child frame.');
+
+promise_test(async t => {
+ const frame = await addSrcdocFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'blocked');
+}, 'WebRTC is blocked in an about:srcdoc child frame.');
+
+promise_test(async t => {
+ const outer = addBlankFrame(t, document);
+ const inner = addBlankFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'blocked');
+}, 'WebRTC is blocked in a nested about:blank frame.');
+
+promise_test(async t => {
+ const outer = await addSrcdocFrame(t, document);
+ const inner = await addSrcdocFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'blocked');
+}, 'WebRTC is blocked in a nested about:srcdoc frame.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers
new file mode 100644
index 0000000..0dd9194
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers
@@ -0,0 +1 @@
+Connection-Allowlist: (response-origin)
\ No newline at end of file
+25
View File
@@ -1,7 +1,32 @@
# S Browser: Anforderungsabgleich und offene Arbeit
**Scope-Änderung vom 13. Oktober 2026:** Maßgeblich ist jetzt
[das Windows-DLP-MVP-Ziel](windows-dlp-mvp.md). Nur Upload-, Clipboard-/Prompt-DLP,
zentrale Durchsetzung und Stabilitäts-/Windows-Abnahme sind aktive Arbeit.
Die breiteren Anforderungen und Erweiterungen unten bleiben Roadmap.
## Aktuelle nächste Abnahmen
Aktueller Integrationsstand: 713 Tests bestehen
(`artifacts/worker-integration-suite.log`). Der echte Admin-Workflow für typisierte
Login-Regeln, unabhängige Veröffentlichung und Auditor-Leserechte ist geprüft:
193 abgeschlossene Anfragen mit korrelierten exportierten Traces, eine abgebrochene
Leseanfrage, keine fehlgeschlagenen Mutationen. Zentrale Worker-Policy und
restriktive Profilsteuerung sind implementiert; die Laufzeitintegration der
unveränderlich zugeordneten Agent-Partitionen ist noch in Arbeit. Der letzte
geprüfte Windows-Build gehört zum vorangegangenen Visual-Export-Stand und ist
kein Paketnachweis für diese neuen Änderungen.
Unabhängige Registry-Prüfung: Ein synchroner Abbruch innerhalb der
Session-Erstellung konnte vor der Registrierung des Abort-Listeners eintreten.
Die Anfrage schlug fehl, während die native Sitzung aktiv blieb. Die
Laufzeitintegration muss diese Race mit Regressionstest schließen. Ebenso sind
begrenzte Revocation-Tombstones und begrenzte Setup-/Cleanup-Wartezeiten zu
prüfen. Requests ohne WebContents dürfen weder über den aktiven Tab noch allein
über fehlende Initiator-Metadaten eine Identität erhalten. Für interne Downloads
ist eine nachweisbar private Transportzuordnung erforderlich; ein allgemeiner
ownerloser Request-Ausnahmepfad ist kein akzeptabler Ersatz.
Gespeicherte Agent-Sitzungen und kontrollierte Credential-Vorbereitung sind für
ihre dokumentierten nativen Pfade integriert und nachgewiesen. Die vorangegangene
Controlled-Login-Abnahme umfasste 668 bestandene Tests, Syntaxprüfung, stabilen Windows-Build,
+171
View File
@@ -0,0 +1,171 @@
# Verbindliches Ziel: Windows-DLP-MVP
Der Nutzer hat am 13. Oktober 2026 den bisherigen breiten Funktionsumfang
ausdrücklich ersetzt. Dieses Ziel hat Vorrang vor älteren Aufgabenlisten:
Ein stabiler, zentral verwalteter Windows-Browser verhindert, dass vertrauliche
Texte oder Dateien durch Uploads, Copy/Paste oder KI-Prompt-Versand an nicht
erlaubte Ziele gelangen. Erst nach der folgenden Abnahme wird das MVP abgeschlossen.
## Abnahmekriterien
1. Ein Administrator kann vertrauliche Textbeispiele und Referenzdokumente
registrieren, Regeln veröffentlichen und erlaubte KI-Ziele konfigurieren.
Der Browser erzwingt die signierte aktuelle Policy; Benutzer können den Schutz
nicht durch lokale Einstellungen abschalten.
2. Paste per Tastatur, Kontextmenü und Seiten-Clipboard wird vor Freigabe geprüft.
Copy und Cut werden ebenfalls vor dem Schreiben in die Windows-Zwischenablage
geprüft. Jeder Paste liest den aktuellen Inhalt, einschließlich Änderungen
durch andere Anwendungen, und gibt ausschließlich den geprüften Snapshot
frei. Die zentrale Policy kann vertrauliche Inhalte und gefährliche
Ausführungsbefehle sperren. Änderungen von Ziel, Auswahl oder Policy während
eines asynchronen Scans dürfen keinen ungeprüften Inhalt freigeben.
Erlaubter öffentlicher Text funktioniert in normalen Eingabefeldern und den
im MVP getesteten Web-Editoren unabhängig von Website oder Anwendung
(Notizen, Webmail, Chat und KI). Vertraulicher Text wird blockiert. Anschließender
Versand, getippter Text und durch Seitenskripte erzeugte Request-Bodies werden
zusätzlich geprüft; Paste-Prüfung allein reicht nicht.
3. Gewöhnliche Windows-Dateiuploads per Dateiauswahl und Drag-and-drop werden
anhand der tatsächlich übertragenen Bytes geprüft. Lokale Klassifikation,
Fingerprints, semantische Erkennung und erforderliches OCR bleiben lokal.
Änderung nach dem Scan darf keine ungeprüften Bytes freigeben.
4. Verschlüsselte, übergroße oder unvollständig geprüfte Dateien sowie nicht
unterstützte Transferwege werden mit verständlichem Grund blockiert.
Fehler, Scanner-Ausfall, abgelaufene Policy und Widerruf dürfen keine
Freigabe vertraulicher Inhalte verursachen. Alternative Netzwerkwege dürfen
die erzwungene DLP-Prüfung nicht umgehen.
5. Echte Windows-/Electron-Tests prüfen für jeden unterstützten Kernweg sowohl
erlaubte Übertragung als auch vertrauliche Inhalte. Ein kontrollierter
Empfänger beweist, dass blockierte Inhalte nicht angekommen sind. Die Tests
schließen Policywechsel, Tab-/Frame-Wechsel und Scannerfehler ein.
6. Block-/Allow-Entscheidungen sind für den Administrator nachvollziehbar.
Debug-Traces korrelieren Prüfung und Freigabe, zeigen Laufzeiten und Fehler;
Passwörter und Tokens gelangen nicht in gewöhnliche Logs. Zentraler
Diagnostikschalter verändert keine DLP-Entscheidung.
7. Ein reproduzierbarer Windows-Build enthält exakt den geprüften Code und die
lokalen Scanner-Runtimes. Enrollment, Policy-Sync, mehrere Tabs, wiederholte
Uploads/Paste und Neustart bestehen einen dokumentierten Stabilitätslauf.
Keine offene kritische Umgehung oder Regression auf diesen Kernwegen.
## Aktueller Abnahmestand
749 Tests bestehen im letzten vollständigen Lauf; ESLint verlangt null Fehler
und null Warnungen. Nachweis: `artifacts/mvp-final-startup-suite.log`.
Die neue native Windows-Abnahme besteht mit 17 Checks, einschließlich echtem
Maus-Klick im contenteditable-Editor, Klartext/HTML-Clipboard, Policy-Race,
YARA-/ICAP-Block und Scanner-Ausfall. Danach startete ein anderer Browserprozess
mit demselben DPAPI-Profil, Gerät und Policy v4, ohne erneutes Enrollment.
Fünf weitere Neustart-Checks bestehen: insgesamt 13 erlaubte Requests,
null vertrauliche Requests und zwei exakt übertragene öffentliche Dateien.
Beide Prozesse exportierten korrelierte Clipboard-, URL- und Upload-Traces
mit null Exportfehlern. Nachweise: `artifacts/windows-dlp-mvp-process-restart-summary.json`,
`windows-dlp-mvp-first-process-native-proof.json` und
`windows-dlp-mvp-restart-native-proof.json`.
Der aktuelle integrierte Relay-/Proxy-/Preload-Stand bestand diese native
Abnahme erneut: `artifacts/windows-dlp-mvp-current-frozen-native.log`.
Das Gateway prüft normalisierte Textdarstellungen einzeln und revalidiert
Policy/Widerruf vor Versand und Ausgabe. Tatsächliche ICAP-/Provider-Tests
blockieren auch codierte Scanner-Marker vor dem Anbieter. Begrenzte
Normalisierung ist kein universeller Deobfuskationsnachweis.
Ein Windows-Build ist abgeschlossen, einschließlich tatsächlicher
Fuse-Werte, eingebettetem ASAR-Header-Hash und Parser-ACL-Provisionierung.
Paket-/Runtime-Vergleich bestanden. Der korrigierte CLI-Inspector-Test und die
manipulierte ASAR-Kopie wurden danach tatsächlich negativ geprüft; die Kopie
wurde vor Anwendungseinstieg abgewiesen. Eine weitere unabhängige Prüfung fand
akzeptierte Zertifikats-/Feature-Bypass-Startparameter. Der Quellstand verweigert
diese inzwischen mit terminalem, festem Fehler. Der erneute Build, vollständige
Paketprüfung und elf zusätzliche negative Startfälle bestehen am tatsächlichen
Paket. Nachweis: `artifacts/package-fuses-proof.json`, einschließlich
`unsafeStartupOverridesRejected=true`. Der Installer bleibt `NotSigned`.
WebTransport ist inzwischen als tatsächlicher Umgehungsweg bestätigt:
HTTP-DLP-Hooks und Abschalt-Flags verhinderten die Übertragung nicht. Ein nativer
Proxy-Zwang wurde dagegen mit null empfangenen Paketen bei weiter funktionierendem
HTTP bewiesen. Der authentifizierte lokale Relay ist inzwischen in Enrollment,
Policywechsel und Quarantäne integriert; Browserverkehr erhält keinen DIRECT-
Fallback. Enterprise-Proxy-Zugangsdaten sind an Host und Port gebunden.
Die vollständige Browser-Abnahme mit echtem WebTransport-Empfänger besteht:
Eine ungeschützte Window-/Worker-Kontrolle übertrug 132 Bytes; der tatsächlich
enrollte Browser übertrug null Pakete und Bytes, auch nach reviewed DIRECT und
HTTPS-PAC→DIRECT-Policywechseln. Drei gewöhnliche HTTPS-POSTs funktionierten.
Nachweis: `artifacts/fullmain-webtransport-native-proof.json`, mit genauen
Quellhashes; spätere Änderungen betreffen ausschließlich Startparameterprüfung.
Die vor HTTP blockierten Versuche liefern keine webRequest-Callbacks und
damit weiterhin keine einzelnen DLP-Audit-Ereignisse. Diese SIEM-Grenze bleibt offen.
Ältere fehlgeschlagene Testläufe bleiben als solche erhalten.
## Verbleibende Abnahmen
Die lokale semantische Browser-Abnahme ist jetzt bestanden: reviewed Teaching
behält 36 Fragment-Fingerprints und Prüfmetadaten, keinen Originaltext. Mit
dem tatsächlichen gepinnten Modell wurden öffentliche Inhalte erlaubt und eine
nichtliteral vertrauliche Paraphrase bei Paste, Textversand und Dateiupload
blockiert. Drei öffentliche Requests und null vertrauliche Paraphrasen erreichten
den Empfänger; eine öffentliche Datei kam exakt an. Nachweise:
`artifacts/windows-semantic-dlp-native-proof.json` und
`artifacts/windows-semantic-teaching-proof.json`.
Neue Stabilitätsgrenze: Chromium 152 kann bei WebRTC in einem initialen
about:blank-Iframe unter der nativen Connection-Allowlist den Renderer beenden.
Der zusätzliche Frame-Preload verhindert entsprechende Konstruktoraufrufe in
srcdoc-, Blob- und Cross-Origin-Frames; initiales about:blank erhält diesen
Preload jedoch nicht. Diese verbleibende Grenze ist kein bestandener
All-Frame-Nachweis. Eine weitere native oder frühere Absicherung ist vor der
entsprechenden Stabilitätsabnahme erforderlich. Der zugehörige Chromium-Fix
ist Commit `46fcf3faed9db394de7046a100301f64c2842f0e`; auch die isoliert
getestete Electron-Nightly vom 13. Oktober enthält ihn noch nicht. Ein bloßes
Versionsupdate oder JavaScript-Wrapper gilt daher nicht als Lösung.
Ein kontextangepasster [Backport](native-runtime/README.md) besteht
`git apply --check` gegen die genaue Chromium-Basis; er ist noch nicht kompiliert
oder nativ abgenommen.
- Aktuelle native Abnahmen zusammenführen und gegen den aktuellen Quellstand
erneut prüfen; ältere grüne Tests belegen keinen später geänderten Build.
- Praktische Paste-/Versand-Coverage weiterer moderner Editoren prüfen.
Einfache contenteditable-Textpfade sind nativ mit erlaubtem Text, vertraulichem
Text und inert eingefügtem HTML geprüft. Komplexer Richtext und undurchsichtige
Clipboard-Formate bleiben konservativ gesperrt; universelle Editor-Kompatibilität
ist damit nicht belegt.
- Nicht unterstützte Hintergrund-/Streaming-/WebSocket-/WebRTC-Pfade auf
tatsächliche Sperre prüfen; umfassende Worker-Unterstützung ist kein MVP-Ziel.
- Race-, Timeout-, Policywechsel- und Scannerfehler-Nachweise vervollständigen.
- Abschließende vollständige Tests, native DLP-Abnahme, Stabilitätslauf,
Windows-Build und Paketprüfung durchführen und Ergebnisse dokumentieren.
Die Liste ist ein Arbeitsauftrag, keine Behauptung, dass jeder Punkt bereits
fehlt oder ein bestätigter Bypass besteht. Der folgende Audit bestimmt die
konkreten Defekte und fehlenden Nachweise.
## Verschoben
## WebSocket- und WebRTC-Ziel
Nutzerpräzisierung: WebSockets und WebRTC sollen nutzbar sein, wenn ihr jeweiliger
Datenweg überprüfbar ist. Pauschale Sperre ist eine vorläufige Schutzmaßnahme,
kein funktionales Endziel. Freigabe muss pro überprüfbarem Kanal erfolgen:
WebSocket-Nachrichten einschließlich binärer/komprimierter Nutzlasten und
WebRTC-DataChannels benötigen Inhaltsprüfung vor Übertragung sowie aktuelle
Policy- und Zielbindung. WebRTC-Audio/Video benötigen eigene Medienregeln und
Prüfnachweise; eine Signalisierungs- oder Domainprüfung belegt keine Inhalts-DLP.
Die vorhandenen Electron-webRequest-Hooks prüfen HTTP-Anfragen und den
WebSocket-Verbindungsaufbau, liefern jedoch keinen allgemeinen nativen Gate für
jede WebSocket-Nachricht oder WebRTC-Nutzlast. Ein JavaScript-Wrapper allein ist
keine belastbare Sicherheitsgrenze. Unterstützte Wege benötigen einen
kontrollierten Transport oder zusätzliche native Chromium-Enforcement-Hooks
mit echten Nachrichten-/Empfänger- und Umgehungstests. Bis dahin bleibt nur der
nicht überprüfbare Weg gesperrt, mit dokumentiertem Kompatibilitätsgrund.
## Weitere Roadmap
Andere Betriebssysteme und Mobile, vollständige KI-Agentensteuerung und eigene
Worker-Sitzungen, weitere Komfort-/Designfunktionen, zusätzliche Vault-Funktionen,
breitere Multimediaanalyse und zusätzliche Admin-Erweiterungen sind Roadmap.
Bestehender Code bleibt erhalten, wird aber nicht zur Voraussetzung dieser
MVP-Abnahme erweitert.
Produktionsrollout bleibt von Pilot-/MVP-Abnahme getrennt: signierter Installer
und Updatekanal, unabhängige Sicherheitsprüfung sowie kundenspezifische IdP-,
Proxy-/ICAP-/SIEM-Integration benötigen eigene Nachweise. Das MVP verspricht
keine universelle Erkennung aller Geheimnisse und keinen Schutz gegen einen
Angreifer mit lokalen Administratorrechten.
+39 -17
View File
@@ -1,25 +1,47 @@
# Native worker ownership: verified boundary and next implementation
# Attributed agent background workers
Current individual-agent mode blocks requests without a guarded native tab owner. This deliberately prevents service workers from borrowing an actor's authority through origin, Referer, renderer messages or a guessed tab. It also limits compatibility with worker-dependent applications.
The central `browser.allowAgentBackgroundWorkers` gate defaults false. The
permission-only `network:background` scope is also explicit in both the agent
baseline and individual grant; it is not a callable agent RPC. User profiles can
inherit or deny this feature (`features.agentBackgroundWorkers`); they cannot
allow it beyond baseline or grant. Applicable denials accumulate.
## Native evidence
Development support is restricted to immutable, nonpersistent native browser
partitions owned by the enrolled device, individual principal and credential
generation. Human/default shared partitions, legacy callers and unattributed
worker traffic remain blocked. Enabling this feature is not general human browser
worker compatibility, persistence or cross-device worker roaming.
A local Windows feasibility experiment used Electron 44.7.0 / Chromium 152.0.7977.130, two independent in-memory native sessions, and the same synthetic loopback origin. Each session registered a service worker and initiated a POST from that worker. Each session's native webRequest listener received only its own request.
Before network release the owner registry must prove the worker's partition,
registration/source and native context; current policy, grant generation/expiry,
destination and DLP remain mandatory. The foreground tab or a renderer-provided
identity cannot establish ownership. Revocation must deny existing worker traffic,
not merely prevent new registrations. No cloud inspection or extra worker scope
bypasses local mandatory inspection.
The worker requests had no webContentsId or requesting frame. Both had the same initiator origin. Their request objects carried no worker version ID or process ID. ServiceWorkers.getAllRunning exposed different renderProcessIds, but those identifiers were absent from webRequest events. Runtime worker version IDs were available as dictionary keys; the worker information values omitted the versionId field advertised in the installed type definition.
## Verification status
Local ignored evidence: `artifacts/worker-partition-feasibility.json` and `.log`. This isolated API experiment did not run production browser guards or establish an OpenTelemetry integration proof.
The current real Electron admin workflow exercised typed login-rule editing,
independent signed publication and auditor read-only access. All 193 completed
API requests had exported traces; one canceled read had no failed mutation.
The collector reported zero rejected writes. Trace examples:
`a0f881d3c99650c53bacf547a191b216` and
`4416533c41ffe3e2adb81af46a30e0d4`.
Electron documents native [webRequest events](https://www.electronjs.org/docs/latest/api/web-request), [service-worker inventory](https://www.electronjs.org/docs/latest/api/service-workers), and [session partitions](https://www.electronjs.org/docs/latest/api/session). The installed `node_modules/electron/electron.d.ts` was checked against the actual runtime.
Focused policy tests verify the disabled default, preservation through signed
publication, strict boolean validation and rejection of `network:background`
as an executable RPC. Eleven registry tests cover immutable ownership,
installation-before-network-use, revocation and retryable failed cleanup.
## Defensible design
The central gate/scope controls start unchecked, and profile schema/enforcement
prove inherit/deny-only behavior. The actual Windows/Electron admin fixture also verifies the worker gate/scope
start off, auditor controls are disabled, and typed login-rule creation goes
through exact signed independent review and policy-history restoration. Its 193
completed API requests export parent-correlated spans; one canceled GET during
identity reload is recorded separately, with no failed mutation. See
`artifacts/agent-grants-history-admin-proof.json`,
`artifacts/agent-grants-history-admin-traces.json` and
`artifacts/credential-submission-admin-controls.png`.
Allocate a separate native session for each immutable device/principal/generation identity. Assign ownership in the main process before creating any native tab or initiating a request. Install the full network, DLP, download, permission, proxy, DNS and preload controls before the partition is used. A worker's owning principal then comes from the listener's native session closure, not page-supplied claims.
Never reassign a partition to another actor or newer generation. Check the current signed grant, device status, policy expiry and destination scopes before inspection, after every awaited stage and immediately before release. Revocation must quarantine the owner's partition and close its connections. Cookies and authentication state should remain isolated unless a separate explicitly authorized sharing design is implemented.
This attributes requests to a principal, not to a specific tab. Shared workers can serve multiple tabs, and service workers may outlive their clients. Any operation requiring an unavailable tab, exact document or visual approval must remain blocked until an independently verified context is available. Worker downloads, immutable streamed request-body inspection, background lifetimes, restart ownership and complete revocation behavior need native proofs.
## Migration surface
The current main process uses one `browserSession` for proxy updates, DNS resolution, permission handlers, upload terminal events, download fetching and will-download, webRequest gates, developer-tools frontends, preload registration, native tab construction, revocation cleanup and VPN quarantine. All relevant uses must become session-aware together. Merely selecting a new session for a tab while leaving these controls global would create an enforcement gap.
Runtime owner-registry migration, actual native worker attribution/revocation
tests, runtime traces and final Windows/package proof are pending. Do not claim worker support solely from a stored policy flag.