Marking Browser Clipboard Writes as Sensitive in Private Contexts

A topical explainer on why browsers need to opt clipboard writes from private contexts into the OS-level “sensitive” flag, and what goes wrong when a single call site forgets to. The specific bug this maps to is currently under Mozilla security embargo, so I’m describing the class of issue and the shape of the fix without identifying the surface or linking the bug.

A note on what’s missing: This post does not link to a Bugzilla bug or a Phabricator revision. The bug is in a restricted security group at Mozilla, which means it is not publicly accessible until Mozilla decides it can be opened. Until then, the technical content here stays at the level of how this class of issue works, not here is the surface and here is the patch. When the bug becomes public, I’ll update this post with the links and the call-site detail.


The class of issue

Modern operating systems all have some form of clipboard history. macOS syncs the clipboard across devices via Universal Clipboard. Windows 10/11 has a clipboard history viewer. Linux clipboard managers do similar things. These features are great for ordinary use. They are also a long-term disclosure surface: anything an application writes to the clipboard can be archived by the OS, sometimes synced across devices, sometimes shown to anyone with access to the machine.

Operating systems give applications a way to opt out of this archiving. The flag goes by different names per OS — there are platform-specific clipboard format hints and per-pasteboard-type sensitivity markers — but the contract is the same: the application is telling the OS not to persist this clipboard write. If you don’t set the flag, the OS treats the write as ordinary and may archive it.

Browsers are responsible for setting the flag whenever the source of the copy is privacy-sensitive. The canonical case is private browsing: anything the user copies out of a private window is, by definition, supposed to be private. Anything that ends up in OS clipboard history from a private window partially defeats the point.


Why this is fragile across a browser codebase

Browsers don’t have one place that writes to the clipboard. They have many: the address bar, the page context menu, the developer tools, the find bar, password managers, and several others. Each of these constructs a clipboard write at its own call site, and each call site is responsible for deciding whether the write is sensitive.

The defaults conspire against you here. The OS clipboard API defaults to “ordinary.” The browser’s clipboard wrapper passes through whatever flag the caller sets. So a call site that doesn’t think about sensitivity at all will, by inheritance from the default, produce an ordinary clipboard write — even when it’s running in a private window where the answer should clearly be “sensitive.”

The result is that the bug class you have to worry about is structural, not algorithmic: some clipboard call site, somewhere in the codebase, that happens to be reachable from a private context, isn’t passing the sensitive flag through. Finding these is grep work. Auditing them is reading work. Fixing them is usually a one-line change at the call site, threading the private-window state through to the clipboard API.


What the fix looks like in general

The shape is consistent across these patches:

  1. Identify the call site. Some surface in the browser is constructing a clipboard write.
  2. Plumb private-context state. The call site needs to know whether the originating window is private. Sometimes this state is already in scope; sometimes it has to be threaded through one or two layers.
  3. Set the flag conditionally. When the originating window is private, mark the clipboard write as sensitive. Otherwise, leave the default.
  4. Add a regression test. Otherwise the fix can silently revert during a future refactor.

The patch ends up being three or four lines of real change. The work was finding the missing call site and convincing yourself there aren’t more like it.


The general lesson

When auditing a browser for OS-clipboard-disclosure issues, the test to walk through is:

  • For each clipboard write surface in the codebase, ask: can this be reached from a private context?
  • For each surface that can: is the sensitive flag being set?
  • For each surface where it isn’t: that’s a candidate disclosure, regardless of whether anyone has noticed.

This is the kind of audit where the work is tedious but the technique is simple. It’s also exactly the kind of audit that the OS API contract should make unnecessary by inverting the default — all clipboard writes are sensitive unless explicitly marked ordinary — but as long as the contract goes the other way, every browser surface needs an explicit code path that remembers to opt in.


What I learned

  1. Privacy bugs hide in the seams between layers. The browser knows it’s in a private window. The OS clipboard API supports a sensitivity flag. Firefox’s clipboard wrapper supports passing it through. All three layers can be correct in isolation while still leaving a bug at the connective tissue between them.

  2. OS clipboard history is an underappreciated long-term disclosure surface. It’s tempting to think of clipboard contents as “the thing that lives in clipboard for a few seconds.” Modern OSes turned that into “the thing that gets archived, possibly synced across devices, possibly shown in a history viewer.” Browsers have to reason about clipboard writes from private contexts as long-term disclosure events, not transient ones.

  3. “Mark as sensitive” should be the default. Every clipboard API I’ve seen makes ordinary writes the default and sensitive writes the explicit case. From a privacy standpoint that’s exactly backwards. As long as the contract goes that way, every surface that writes to the clipboard from a privacy-sensitive context needs an explicit code path that remembers to opt in. That’s a recurring source of bugs across applications, not just browsers.

  4. Embargoed security bugs are why this post is missing IDs. Mozilla files certain classes of bug in restricted groups so that fixes can land in release before the bug becomes public. That’s the right policy. It also means the writeup has to live at the class-of-issue level until the embargo lifts. When that happens, I’ll backfill the references.