Skip to content

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.

ClaimProved by
No message can name a file to openbrowser::tests::there_is_no_message_that_names_a_file_to_open
…asserted end to end as wellnothing_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.

ClaimProved by
Only the allowlisted extension is servedbrowser::tests::only_the_allowlisted_extension_is_served
An empty allowlist serves nobody, rather than everybodysame test
An extension that is not ours is refused and served nothingan_extension_that_is_not_ours_is_refused_and_served_nothing
An upload before the extension identifies itself is refusedan_upload_before_the_extension_identifies_itself_is_refused
A message this host does not understand is refused, not guessed atbrowser::tests::a_message_this_host_does_not_understand_is_refused_not_guessed_at
An oversized length is refused before anything is allocatedbrowser::tests::an_oversized_length_is_refused_before_anything_is_allocated
Content that is not base64 is refusedbrowser::tests::content_that_is_not_base64_is_refused
A truncated body is an I/O error, not a panicbrowser::tests::a_truncated_body_is_an_io_error_not_a_panic
A closed pipe is not an errorbrowser::tests::a_closed_pipe_is_not_an_error
With no agent running, the host exits and uploads are allowedwith_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 ​

ClaimProved by
A block on an endpoint that cannot block does not become one herebrowser::tests::a_block_on_an_endpoint_that_cannot_block_does_not_become_one_here
A quarantine becomes a block, not a quarantine that never happenedbrowser::tests::a_quarantine_becomes_a_block_rather_than_a_quarantine_that_never_happened
Asking for a justification becomes a warning the person can seebrowser::tests::asking_for_a_justification_becomes_a_warning_the_person_can_see
Auditing does not stop anybodybrowser::tests::auditing_does_not_stop_anybody
A verdict carries a trace id for the person who gets blockedbrowser::tests::a_verdict_carries_a_trace_id_for_the_person_who_gets_blocked
A tenant that wrote no policy is told to stop askinga_tenant_that_wrote_no_policy_is_told_to_stop_asking
An upload above the cap is cut and says sobrowser::tests::an_upload_above_the_cap_is_cut_and_says_so
The extension's own truncation is carried throughbrowser::tests::the_extensions_own_truncation_is_carried_through
A destination is reduced to the thing a policy keys onbrowser::tests::a_destination_is_reduced_to_the_thing_a_policy_keys_on
A destination that is not an upload destination is refusedbrowser::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.

GapState
Host manifest and machine-wide browser registrationImplemented on Windows. dlp-agent install writes the manifest and registers Chrome, Edge and Chromium; uninstall removes both (DLP-236)
Force-install policy templatesImplemented. Group Policy, Intune and .reg examples take an extension id as a parameter (DLP-236)
Signed package and published extension idAbsent. Requires the organisation's signing identity and store account (DLP-1203); the shipped placeholder deliberately resolves to nothing
FirefoxHost 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 coverageImplemented 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.mjs drives 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, .pptx or 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.

Statements here describe what is built and tested; planned work is marked as planned.