Appearance
Browser protection, claim by claim
WIN-DLP-EPIC-015 asks for a managed browser extension and native-messaging host that asks the agent before an upload, because without an extension there is no honest way to see a browser upload, and claiming otherwise is the commonest false claim in this market.
The mechanism is implemented on Linux and Windows: an upload is held open until the agent answers, and it sees all three gestures that put a file into a page: a picker, a drop and a paste (DLP-271). On Windows the host connects to the LocalSystem-owned named pipe, the service establishes its user from the pipe token, and the policy runner returns the verdict (DLP-1256). The host also writes the current focused host into the person's profile, but only while the active-time policy asks for it (DLP-1246). This path is covered by the shared host integration tests and Windows cross-compilation; it has not been driven in a managed Chrome or Edge on a physical Windows host. One external deployment piece is still absent: the repository cannot sign and publish the extension or assign its store id without the organisation's code-signing identity and store account (DLP-1203).
SPIKE-010 records the decisions behind all of it - what the extension can see, how the caller is pinned twice and why neither pinning is what makes it safe, the 512 KiB cap, and the fact that the channel fails open when the agent does not answer.
Implementation: agent/browser-extension/ for the browser half, agent/core/src/browser.rs for the protocol, dlp-agent browser-host for the native-messaging host.
1. The design decision the rest rests on
The extension sends the file's bytes. It never sends a path. (ADR-0014)
The obvious design names the file and lets the host open it. That turns a compromised or impersonating extension into an arbitrary file read as whoever the host runs as - on Windows, possibly the account holding the device key.
| Claim | Proved by |
|---|---|
| No message can name a file to open | browser::tests::there_is_no_message_that_names_a_file_to_open |
| …asserted end to end as well | nothing_the_extension_sends_can_name_a_file_to_open (agent/core/tests/browser_host_cli.rs) |
FromExtension has no variant carrying a path, and a test fails if one is added. That is the right shape for a rule of this kind: not a review comment, a compile-time-adjacent assertion.
2. The host is a courier, not a decider
dlp-agent browser-host runs in the person's own session, with their privileges, in a process they can attach a debugger to. It holds no policy, evaluates nothing and opens no file.
| Claim | Proved by |
|---|---|
| Only the allowlisted extension is served | browser::tests::only_the_allowlisted_extension_is_served |
| An empty allowlist serves nobody, rather than everybody | same test |
| An extension that is not ours is refused and served nothing | an_extension_that_is_not_ours_is_refused_and_served_nothing |
| An upload before the extension identifies itself is refused | an_upload_before_the_extension_identifies_itself_is_refused |
| A message this host does not understand is refused, not guessed at | browser::tests::a_message_this_host_does_not_understand_is_refused_not_guessed_at |
| An oversized length is refused before anything is allocated | browser::tests::an_oversized_length_is_refused_before_anything_is_allocated |
| Content that is not base64 is refused | browser::tests::content_that_is_not_base64_is_refused |
| A truncated body is an I/O error, not a panic | browser::tests::a_truncated_body_is_an_io_error_not_a_panic |
| A closed pipe is not an error | browser::tests::a_closed_pipe_is_not_an_error |
| With no agent running, the host exits and uploads are allowed | with_no_agent_running_the_host_exits_and_uploads_are_allowed |
The empty-allowlist row is the one worth pausing on. An allowlist that treats "nothing configured" as "allow everything" is the standard way this kind of check is defeated, and it has its own assertion.
3. The verdict respects what the endpoint can actually do
| Claim | Proved by |
|---|---|
| A block on an endpoint that cannot block does not become one here | browser::tests::a_block_on_an_endpoint_that_cannot_block_does_not_become_one_here |
| A quarantine becomes a block, not a quarantine that never happened | browser::tests::a_quarantine_becomes_a_block_rather_than_a_quarantine_that_never_happened |
| Asking for a justification becomes a warning the person can see | browser::tests::asking_for_a_justification_becomes_a_warning_the_person_can_see |
| Auditing does not stop anybody | browser::tests::auditing_does_not_stop_anybody |
| A verdict carries a trace id for the person who gets blocked | browser::tests::a_verdict_carries_a_trace_id_for_the_person_who_gets_blocked |
| A tenant that wrote no policy is told to stop asking | a_tenant_that_wrote_no_policy_is_told_to_stop_asking |
| An upload above the cap is cut and says so | browser::tests::an_upload_above_the_cap_is_cut_and_says_so |
| The extension's own truncation is carried through | browser::tests::the_extensions_own_truncation_is_carried_through |
| A destination is reduced to the thing a policy keys on | browser::tests::a_destination_is_reduced_to_the_thing_a_policy_keys_on |
| A destination that is not an upload destination is refused | browser::tests::a_destination_that_is_not_an_upload_destination_is_refused |
The quarantine row is a small thing done right: an endpoint that cannot quarantine a browser upload does not silently drop the instruction, it does the nearest thing it can do and the event records which.
The trace-id row is the one a support desk will care about. A person blocked from uploading something gets a reference, so the conversation starts with a lookup rather than with a description of a dialog.
4. Deployment state
The host side is deployable; the extension package is not yet publishable.
| Gap | State |
|---|---|
| Host manifest and machine-wide browser registration | Implemented on Windows. dlp-agent install writes the manifest and registers Chrome, Edge and Chromium; uninstall removes both (DLP-236) |
| Force-install policy templates | Implemented. Group Policy, Intune and .reg examples take an extension id as a parameter (DLP-236) |
| Signed package and published extension id | Absent. Requires the organisation's signing identity and store account (DLP-1203); the shipped placeholder deliberately resolves to nothing |
| Firefox | Host side implemented; package unsigned; never run in Firefox (DLP-1207). A Firefox add-on id in browser_extension_ids (a GUID in braces, or name@domain) gets its own host manifest, app.mydlp.agent.firefox.json, with allowed_extensions. It is registered under HKLM\SOFTWARE\Mozilla\NativeMessagingHosts, and removed on uninstall. Register, the doctor count and uninstall were run against a real registry through reg.exe, under Wine 9. That opt-in test (browser::registry) is also a step in the windows-agent CI job, which has never run, because no GitHub-hosted job ran from 2026-09-15 to 2026-10-01 (DLP-1215), and none of the Windows jobs has passed since (Windows CI status). The agent recognises Firefox's launch: the manifest path, then the add-on id. manifest.firefox.json is the extension for Firefox, and deploy/browser/firefox-policies.json force-installs it. Still missing: the XPI has to be signed by Mozilla, which needs an add-ons account in the organisation's name, as DLP-1203 does for the stores. Nothing has been run in a managed Firefox on Windows, and whether Firefox grants the extension's host permissions on a policy install is unverified |
| Runtime Windows coverage | Implemented in code; physical-host proof absent. The named pipe serves browser hosts beside the session agent. The capability becomes monitor/warn/block only while a configured extension has connected and a policy names the channel. The shared host integration suite drives the framing, upload and verdict path; Windows cross-compilation covers the platform arm, and a native Windows test drives a host's greeting, an upload and its verdict through the real named-pipe server under Wine (a_browser_upload_is_judged_and_answered_by_the_windows_server). A managed Chrome/Edge run on Windows remains required before release acceptance (DLP-1256) |
A held upload under a rule that asks (DLP-1864). The agent answers hold at once, so the host and the extension keep the upload past their few seconds, and asks the person on the cyberkap screens in the browser's own Windows session. The verdict that follows is allow for a justification, and block for anything else. That covers an exception request (raised as for a file), a cancel, no answer, and an agent or host that goes away while the upload is held: a held upload is never let through by a clock. It happens only for an extension that says supports_hold in its hello, because an older one would read hold as allow. Such an extension, or an upload with no session the pipe could name, is answered at once as before. The service, host and extension halves are tested, the Windows pipe path under Wine. A managed browser has not run it, since that needs DLP-1203's signed id.
The capability matrix reports browser_upload unavailable on any endpoint where no host has connected, rather than implying otherwise - so an undeployed extension shows as an absent capability rather than as a silent one.
5. Gesture coverage, and its deliberate boundary
content.js covers the three user gestures that put a file into a web application: selecting it through <input type="file">, dropping it onto the page, and pasting a clipboard item that is itself a file (DLP-271). agent/browser-extension/test/run.mjs drives all three paths in Chrome and asserts that a blocked file never reaches the application's own handler.
A page that constructs a Blob in JavaScript and sends it with fetch or XMLHttpRequest is deliberately not intercepted. There is no file gesture to observe; catching it means rewriting the page's networking, which breaks sites, is visible and defeatable by the page, and is not a control this product claims.
6. What is not covered
- The extension is tested in real Chrome, but not in a managed Windows browser.
agent/browser-extension/test/run.mjsdrives picker, drop and paste paths and asserts a blocked file never reaches the page handler. The Windows registration and policy decisions are pure tests; no published id exists to exercise a managed installation end to end. - The agent records the verdict it returned, not that the browser obeyed it. Enforcement happens inside the extension and an endpoint cannot confirm from outside that it happened. Recorded in
KNOWN_LIMITATIONS.md. - An upload nobody answers is allowed. If the agent is not running or does not answer within the bounded wait, the extension proceeds - because a browser that hangs on every upload is a browser whose extension is removed within a day.
- Only the first 512 KiB is scanned, the event says the scan was partial, and a partial scan that found nothing is not a statement that the file is clean.
- An uploaded document is read the way a file on disk is (
DLP-1123): by its true type, through the same extractor and limits, so the text inside a.docx,.xlsx,.pptxor PDF is what the detectors see. Until then it was scanned as its compressed bytes, and a card number inside an uploaded Word document was not found. Content the extractor cannot read, including a document cut off by the 512 KiB bound (a truncated archive does not open), is still scanned as bytes, as before, and is reported as not fully scanned.