---
title: "The Extension Was Sandboxed. The Native Host Was Not."
description: "Browser Native Messaging crosses from an untrusted page through an extension into an operating-system process. This research method maps every sender, message, host manifest, parser, native operation, and resulting effect before calling the bridge secure."
date: 2026-08-31
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/browser-native-messaging-trust-boundary/
---

## The browser boundary in 60 seconds

A browser extension can be correctly packaged, signed, reviewed, and isolated from a web page while
still exposing a dangerous path into the operating system.

Native Messaging is intentionally powerful. A browser launches a registered native application as
a separate process and exchanges length-prefixed JSON messages over standard input and output. The
native host can then reach files, devices, credentials, sockets, child processes, and local
management interfaces that ordinary web content cannot.

That design does not create a vulnerability by itself. The vulnerability appears when several
individually valid decisions are treated as one authorization:

1. a page is allowed to run a content script;
2. the content script may message the extension service worker;
3. the service worker may connect to a registered native host;
4. the native host manifest allows that extension ID;
5. the host accepts a JSON operation;
6. the operating system permits the resulting effect.

None of those decisions proves that the original page, frame, user gesture, object, path, or
operation was authorized.

The central research question is:

> Where does web-controlled intent become a native operating-system effect, and which component
> makes the final authorization decision?

This write-up builds a repeatable method for answering that question without turning a local proof
into an unsafe real-world exploit.

## One feature, five different principals

<figure class="diagram">
<svg viewBox="0 0 780 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Trust boundary map from an untrusted web page through a content script and extension service worker into a native messaging host and operating-system effects.">
<defs>
<marker id="bnm-map-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-accent" /></marker>
<marker id="bnm-map-r" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker>
</defs>
<text x="4" y="20" class="dg-accent">BROWSER → NATIVE AUTHORITY GRAPH</text>
<text x="4" y="48" class="dg-muted">ISOLATION CHANGES EXECUTION CONTEXT. IT DOES NOT AUTHORIZE THE NEXT EFFECT.</text>
<rect x="0" y="78" width="136" height="92" rx="9" class="dg-box-crit"/>
<text x="16" y="106" class="dg-crit">WEB PAGE</text><text x="16" y="133" class="dg-mono">origin · frame</text><text x="16" y="156" class="dg-mono">DOM · user input</text>
<line x1="138" y1="124" x2="170" y2="124" class="dg-line-crit" marker-end="url(#bnm-map-r)"/>
<rect x="174" y="78" width="138" height="92" rx="9" class="dg-box"/>
<text x="190" y="106" class="dg-label">CONTENT SCRIPT</text><text x="190" y="133" class="dg-mono">isolated world</text><text x="190" y="156" class="dg-muted">LESS TRUSTED</text>
<line x1="314" y1="124" x2="346" y2="124" class="dg-line-accent" marker-end="url(#bnm-map-a)"/>
<rect x="350" y="78" width="150" height="92" rx="9" class="dg-box-accent"/>
<text x="366" y="106" class="dg-label">SERVICE WORKER</text><text x="366" y="133" class="dg-mono">extension APIs</text><text x="366" y="156" class="dg-accent">POLICY POINT</text>
<line x1="502" y1="124" x2="534" y2="124" class="dg-line-accent" marker-end="url(#bnm-map-a)"/>
<rect x="538" y="78" width="116" height="92" rx="9" class="dg-box"/>
<text x="554" y="106" class="dg-label">STDIO JSON</text><text x="554" y="133" class="dg-mono">length frame</text><text x="554" y="156" class="dg-muted">SERIALIZED</text>
<line x1="656" y1="124" x2="688" y2="124" class="dg-line-crit" marker-end="url(#bnm-map-r)"/>
<rect x="692" y="78" width="88" height="92" rx="9" class="dg-box-crit"/>
<text x="706" y="106" class="dg-crit">HOST</text><text x="706" y="133" class="dg-mono">native UID</text><text x="706" y="156" class="dg-mono">OS access</text>
<rect x="0" y="224" width="242" height="126" rx="9" class="dg-box"/>
<text x="18" y="254" class="dg-label">Browser-side facts</text>
<text x="18" y="282" class="dg-mono">sender.id · sender.url · tab.id</text>
<text x="18" y="307" class="dg-mono">frameId · documentId · gesture</text>
<text x="18" y="332" class="dg-muted">LOST IF NOT BOUND TO REQUEST</text>
<rect x="269" y="224" width="242" height="126" rx="9" class="dg-box-accent"/>
<text x="287" y="254" class="dg-label">Native request contract</text>
<text x="287" y="282" class="dg-mono">version · operation · resource</text>
<text x="287" y="307" class="dg-mono">nonce · expiry · approval</text>
<text x="287" y="332" class="dg-accent">ALLOWLISTED SCHEMA</text>
<rect x="538" y="224" width="242" height="126" rx="9" class="dg-box-crit"/>
<text x="556" y="254" class="dg-label">Operating-system effect</text>
<text x="556" y="282" class="dg-mono">file · process · device</text>
<text x="556" y="307" class="dg-mono">credential · socket · config</text>
<text x="556" y="332" class="dg-crit">INDEPENDENTLY VERIFIED</text>
<line x1="242" y1="287" x2="265" y2="287" class="dg-line-accent" marker-end="url(#bnm-map-a)"/>
<line x1="511" y1="287" x2="534" y2="287" class="dg-line-crit" marker-end="url(#bnm-map-r)"/>
<text x="4" y="398" class="dg-muted">THE HOST MANIFEST SELECTS AN EXTENSION. IT DOES NOT PRESERVE THE ORIGINAL WEB AUTHORIZATION CONTEXT.</text>
</svg>
<figcaption>The extension ID is only one principal in the chain. Page origin, frame, extension component, native process identity, requested object, and resulting effect must remain distinguishable.</figcaption>
</figure>

Chrome documents content scripts as isolated from the page's JavaScript environment. That is an
execution isolation property. Chrome's own messaging guidance separately says content scripts are
less trustworthy than the extension service worker and that messages from them should be treated
as attacker-crafted.

The distinction matters. An isolated content script may still read page-controlled DOM values,
receive `window.postMessage` events, or be triggered on a compromised origin. If the service worker
forwards its message without validating `sender` and the requested operation, the isolated world
becomes a relay rather than a security boundary.

## What Native Messaging actually guarantees

The platform provides useful controls, but each control answers a narrow question.

| Mechanism                                     | What it establishes                                                 | What it does not establish                                                   |
| --------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Extension `nativeMessaging` permission        | The extension may use the Native Messaging API                      | That every extension component should invoke every native operation          |
| Host `allowed_origins` / `allowed_extensions` | The named extension ID may start the host                           | Which page, frame, tenant, or user workflow originated the request           |
| Isolated content-script world                 | Page JavaScript cannot directly read the content script's variables | That data derived from the page is trustworthy                               |
| `sender.id` and `sender.url`                  | Browser-observed sender context for a message                       | That an operation, path, or object is authorized                             |
| Length-prefixed JSON                          | A transport format with message boundaries                          | A safe schema, semantic authorization, replay protection, or resource limits |
| Native process UID                            | The OS identity under which the host executes                       | That the browser caller should inherit all of that identity's access         |
| Browser extension review                      | A distribution and policy checkpoint                                | The correctness of the separately installed native application               |

Chrome launches the native host as a separate process and communicates over stdin/stdout. Messages
are UTF-8 JSON prefixed by a 32-bit length in native byte order. Chrome currently documents a
64 MiB browser-to-host limit and a 1 MiB host-to-browser limit. Those are transport ceilings, not
safe application limits. A host expecting a 2 KiB command envelope should enforce 2 KiB.

Chrome passes the calling extension origin as the first host argument. Firefox uses a related but
different contract and host-manifest key. Cross-browser compatibility code must not silently
collapse those identities or assume serialization behavior is identical: Chrome extension
messaging uses JSON serialization while other implementations may use structured clone.

## The vulnerability pattern: origin laundering

The most common failure is not a memory-corruption bug. It is **origin laundering**:

```text
untrusted page value
  → content-script object
  → extension message
  → native JSON request
  → local file or process effect
```

At every arrow, the next component sees a request from a more trusted principal:

- the service worker sees its own content script;
- the browser sees an installed extension;
- the native host sees an allowed extension origin;
- the operating system sees a local native process.

If the original page and resource authorization are not carried and revalidated, trust increases
while context disappears.

This creates several recurring vulnerability classes:

| Class                           | Dangerous design                                                      | Resulting risk                                         |
| ------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------ |
| Confused deputy                 | Any content script message is forwarded to the host                   | A page borrows extension and host authority            |
| Missing object authorization    | The caller supplies an arbitrary path, device ID, profile, or account | Cross-user or cross-tenant access                      |
| Argument injection              | The host concatenates message fields into a shell or command line     | Native code execution under the host identity          |
| Path traversal / link following | A relative path is joined and opened without final-object validation  | Read or write outside the intended root                |
| Replay                          | A privileged request has no nonce, expiry, or state binding           | A captured message repeats a prior effect              |
| Parser asymmetry                | Browser and host coerce types or sizes differently                    | Validation occurs on a different object than execution |
| Lifecycle confusion             | Long-lived ports retain state across navigation or frame replacement  | New page context inherits old authorization            |
| Installation boundary failure   | A writable manifest or host path redirects the registered binary      | Local persistence or privilege crossing                |

## A bounded synthetic lab

The lab needs no production extension and no third-party target. It uses:

- one unpacked Manifest V3 extension;
- one local page at `https://lab.invalid` or a loopback test origin;
- one native host registered only for the lab extension ID;
- one canary directory containing non-sensitive test files;
- one operation that reads a named canary;
- one independent observer that records which file was actually opened.

The unsafe service worker below illustrates the trust loss:

```js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type !== "native") return;

  chrome.runtime.sendNativeMessage(
    "com.jankesec.native_lab",
    request.payload,
    (response) => sendResponse(response),
  );
  return true;
});
```

It checks a routing label and nothing else. It does not validate the sending extension component,
page origin, frame, operation, object, size, state, or user gesture.

A deliberately vulnerable synthetic host makes the second failure explicit:

```python
from pathlib import Path

def handle(message):
    if message.get("op") == "read_file":
        return {"data": Path(message["path"]).read_text()}
    return {"error": "unknown operation"}
```

This is not a complete exploit. It is a minimal authorization oracle. The positive test requests
`/tmp/nmh-lab/canary.txt`. The negative test requests a different canary outside the approved root.
The finding exists only if the second request reaches the host and the independent file observer
confirms that the wrong object was opened.

Do not demonstrate impact with real browser data, SSH keys, password stores, or user documents.
The vulnerability is the unauthorized object transition, not the sensitivity of a borrowed file.

<figure class="evidence">
  <picture>
    <source
      type="image/webp"
      srcset="/images/posts/responsive/browser-native-messaging-boundary-cutaway-768.webp 768w, /images/posts/responsive/browser-native-messaging-boundary-cutaway-1200.webp 1200w, /images/posts/browser-native-messaging-boundary-cutaway.webp 1600w"
      sizes="(max-width: 820px) 94vw, 900px"
    />
    <img
      src="/images/posts/browser-native-messaging-boundary-cutaway.webp"
      alt="Architectural cutaway showing a web page crossing isolated extension chambers and a framed message conduit before reaching native filesystem and process machinery."
      loading="lazy"
      decoding="async"
      width="1600"
      height="900"
    />
  </picture>
  <figcaption>
    <span>Boundary plate / 01</span>
    The browser-to-native path is not one pipe. Each chamber changes principal, available context, and authority.
  </figcaption>
</figure>

## Inventory before interaction

Start with static evidence. Record both extension and host installation state before sending a
message.

### Extension inventory

| Field                           | Evidence                                                           |
| ------------------------------- | ------------------------------------------------------------------ |
| Extension ID and install source | Browser extension page, enterprise policy, signed package metadata |
| Manifest version and update URL | `manifest.json` and managed-extension policy                       |
| Permissions                     | `permissions`, `optional_permissions`, `host_permissions`          |
| Content-script reach            | Match patterns, excluded matches, frames, execution world          |
| External callers                | `externally_connectable` IDs and page patterns                     |
| Message listeners               | `onMessage`, `onConnect`, external variants, port names            |
| Native operations               | Every `connectNative` and `sendNativeMessage` call site            |
| Sender checks                   | `sender.id`, URL origin, tab, frame, document, incognito state     |
| State binding                   | Gesture, nonce, expiry, selected object, active tab, tenant        |

### Native host inventory

| Field                       | Evidence                                                          |
| --------------------------- | ----------------------------------------------------------------- |
| Manifest location and owner | OS path or registry key, ownership and ACL                        |
| Host executable path        | Absolute resolved path and final file identity                    |
| Allowed extension IDs       | Exact `allowed_origins` or `allowed_extensions` values            |
| Runtime identity            | UID, token, integrity level, sandbox, service account             |
| Parser                      | Length handling, UTF-8 decoding, JSON type checks, maximum size   |
| Dispatcher                  | Operation allowlist and default-deny behavior                     |
| Object authorization        | How resource IDs become paths, devices, or accounts               |
| Process creation            | Shell use, argument arrays, environment, working directory        |
| Output behavior             | Data classification, truncation, errors, stderr logging           |
| Update path                 | Installer identity, signature verification, writable parent paths |

This inventory frequently closes false positives. A host may look privileged but expose only a
fixed status query. Conversely, a user-level host may reach browser profiles, developer
credentials, cloud CLIs, and local sockets that create a high-impact path.

## Preserve the browser context in the request

The service worker is the first reliable policy point. It should turn browser-observed facts into a
small request contract rather than forward a page-defined object.

```js
const ALLOWED_ORIGINS = new Set(["https://lab.invalid"]);

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  const origin = sender.url ? new URL(sender.url).origin : "";
  const validSender =
    sender.id === chrome.runtime.id &&
    sender.tab?.id !== undefined &&
    sender.frameId === 0 &&
    ALLOWED_ORIGINS.has(origin);

  if (!validSender || message?.type !== "read-canary") {
    sendResponse({ ok: false, error: "denied" });
    return false;
  }

  const request = {
    version: 1,
    operation: "canary.read",
    resource: String(message.resource),
    origin,
    tabId: sender.tab.id,
    documentId: sender.documentId,
    nonce: crypto.randomUUID(),
    expiresAt: Date.now() + 5_000,
  };

  chrome.runtime.sendNativeMessage(
    "com.jankesec.native_lab",
    request,
    sendResponse,
  );
  return true;
});
```

This is stronger, not complete. The native host still cannot blindly trust every field merely
because the extension created it. The contract needs a fixed schema, bounded values, a short
lifetime, replay tracking where effects matter, and an operation-to-resource authorization table.

Avoid using user-supplied paths in the browser contract. Use stable resource identifiers such as
`canary-a` and resolve them inside the host:

```python
RESOURCES = {
    "canary-a": Path("/tmp/nmh-lab/canary-a.txt"),
    "canary-b": Path("/tmp/nmh-lab/canary-b.txt"),
}

def handle(message):
    if set(message) != {
        "version", "operation", "resource", "origin",
        "tabId", "documentId", "nonce", "expiresAt",
    }:
        return {"ok": False, "error": "schema"}

    if message["version"] != 1 or message["operation"] != "canary.read":
        return {"ok": False, "error": "operation"}

    path = RESOURCES.get(message["resource"])
    if path is None:
        return {"ok": False, "error": "resource"}

    return {"ok": True, "data": path.read_text()}
```

For higher-impact operations, a JSON field claiming `origin` is not proof. Use a brokered session
established from browser-observed state, bind approval to the resolved operation and object, and
keep the native capability narrower than the host's ambient OS identity.

## Parse the transport as hostile input

A native host parser should read exactly four header bytes, reject oversized messages before
allocation, read exactly the declared body, decode strict UTF-8, require a JSON object, validate
types, and produce one framed response.

```python
import json
import struct
import sys

MAX_REQUEST = 16 * 1024

def read_exact(stream, count):
    chunks = bytearray()
    while len(chunks) < count:
        part = stream.read(count - len(chunks))
        if not part:
            raise EOFError("truncated native message")
        chunks.extend(part)
    return bytes(chunks)

def read_message():
    header = read_exact(sys.stdin.buffer, 4)
    length = struct.unpack("@I", header)[0]
    if length == 0 or length > MAX_REQUEST:
        raise ValueError("invalid message length")

    payload = read_exact(sys.stdin.buffer, length)
    value = json.loads(payload.decode("utf-8", errors="strict"))
    if not isinstance(value, dict):
        raise TypeError("message must be an object")
    return value
```

Important parser tests include:

- zero, one-byte, truncated, and oversized headers;
- declared length shorter or longer than the actual body;
- invalid UTF-8;
- duplicate or unexpected fields;
- arrays or scalars instead of an object;
- numbers where a string identifier is expected;
- deeply nested JSON and excessive collection counts;
- multiple frames in one stream;
- clean EOF versus mid-frame EOF;
- stdout contamination by debug logs.

Debug output belongs on stderr. One accidental print to stdout corrupts the framing channel.

## Authorization must survive every hop

<figure class="diagram">
<svg viewBox="0 0 780 446" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A decision pipeline that validates browser sender context, resolves a fixed native operation and resource, enforces policy, executes with a narrow capability, verifies the effect, and records a denial when any gate fails.">
<defs><marker id="bnm-gate-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-accent"/></marker></defs>
<text x="4" y="20" class="dg-accent">NATIVE EFFECT DECISION PIPELINE</text>
<text x="4" y="48" class="dg-muted">VALIDATE CONTEXT BEFORE CREATING A NATIVE CAPABILITY</text>
<rect x="0" y="76" width="142" height="92" rx="9" class="dg-box"/>
<text x="16" y="104" class="dg-label">1 · SENDER</text><text x="16" y="131" class="dg-mono">origin · frame</text><text x="16" y="154" class="dg-mono">document · gesture</text>
<line x1="144" y1="122" x2="164" y2="122" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<rect x="168" y="76" width="142" height="92" rx="9" class="dg-box"/>
<text x="184" y="104" class="dg-label">2 · SCHEMA</text><text x="184" y="131" class="dg-mono">version · types</text><text x="184" y="154" class="dg-mono">size · unknown keys</text>
<line x1="312" y1="122" x2="332" y2="122" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<rect x="336" y="76" width="142" height="92" rx="9" class="dg-box-accent"/>
<text x="352" y="104" class="dg-label">3 · RESOLVE</text><text x="352" y="131" class="dg-mono">operation ID</text><text x="352" y="154" class="dg-accent">resource ID</text>
<line x1="480" y1="122" x2="500" y2="122" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<rect x="504" y="76" width="126" height="92" rx="9" class="dg-box-accent"/>
<text x="520" y="104" class="dg-label">4 · POLICY</text><text x="520" y="131" class="dg-mono">allow · expiry</text><text x="520" y="154" class="dg-accent">replay gate</text>
<line x1="632" y1="122" x2="652" y2="122" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<rect x="656" y="76" width="124" height="92" rx="9" class="dg-box-accent"/>
<text x="672" y="104" class="dg-label">5 · EXECUTE</text><text x="672" y="131" class="dg-mono">narrow UID</text><text x="672" y="154" class="dg-accent">no shell</text>
<rect x="0" y="224" width="230" height="100" rx="9" class="dg-box-crit"/>
<text x="18" y="254" class="dg-crit">DENIAL RECORD</text><text x="18" y="281" class="dg-mono">gate · reason · request ID</text><text x="18" y="306" class="dg-mono">no operation · no side effect</text>
<rect x="276" y="224" width="230" height="100" rx="9" class="dg-box-accent"/>
<text x="294" y="254" class="dg-label">EFFECT VERIFIER</text><text x="294" y="281" class="dg-mono">expected object changed?</text><text x="294" y="306" class="dg-accent">unexpected object unchanged?</text>
<rect x="550" y="224" width="230" height="100" rx="9" class="dg-box"/>
<text x="568" y="254" class="dg-label">EVIDENCE BUNDLE</text><text x="568" y="281" class="dg-mono">request · decision · effect</text><text x="568" y="306" class="dg-mono">negative control · cleanup</text>
<path d="M718 170 C718 200 666 204 666 220" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<line x1="508" y1="274" x2="546" y2="274" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<path d="M386 170 C386 192 391 200 391 220" class="dg-line-accent" marker-end="url(#bnm-gate-a)"/>
<text x="4" y="378" class="dg-muted">FAIL CLOSED WHEN:</text>
<rect x="0" y="392" width="780" height="42" rx="7" class="dg-box-crit"/>
<text x="18" y="418" class="dg-mono">UNKNOWN SENDER · STALE DOCUMENT · EXTRA FIELD · UNKNOWN RESOURCE · REPLAY · AMBIENT ADMIN · EFFECT MISMATCH</text>
</svg>
<figcaption>A valid extension origin is only the first gate. The system creates native authority only after sender, schema, object, policy, runtime identity, and expected effect agree.</figcaption>
</figure>

## Evidence matrix

A defensible result separates observed fact, inference, positive execution, negative control, and
remaining uncertainty.

| Claim                                     | Positive evidence                                                | Negative control                                                   | Strong conclusion                                                |
| ----------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| The page can reach the extension listener | Browser-observed message with tab, frame, document, and origin   | Same message from a non-matching origin is rejected                | Reachability is limited to the recorded sender set               |
| The extension can start the host          | Host process creation correlated to request ID                   | Unlisted extension ID receives an access denial                    | Host manifest filters extension identity                         |
| The host exposes an operation             | Valid schema returns the expected canary result                  | Unknown operation and extra field are rejected                     | Dispatcher is explicit and default-deny                          |
| Resource authorization works              | Allowed resource ID opens the mapped canary                      | Traversal, absolute path, symlink, and unknown ID fail             | Caller cannot select an arbitrary object through tested variants |
| Replay is controlled                      | Fresh nonce succeeds once                                        | Same nonce and expired request fail with no effect                 | Tested privileged request is single-use within its lifetime      |
| Native authority is bounded               | Runtime identity and system call trace match expected object     | Observer confirms protected and unrelated objects remain untouched | Demonstrated effect is limited to the tested capability          |
| Cleanup is complete                       | Lab manifest, extension, canaries, process, and logs are removed | Reconnection attempt fails after cleanup                           | Test state no longer exposes the lab bridge                      |

Do not claim that a denied JSON response proves no effect occurred. Verify the canary, filesystem
event, process tree, or device state independently.

## Test matrix

| Layer            | Test                                                                  | Expected secure result                             |
| ---------------- | --------------------------------------------------------------------- | -------------------------------------------------- |
| Web origin       | Same message from allowed and unallowed origins                       | Only the exact allowed origin proceeds             |
| Frame            | Top frame versus embedded or stale frame                              | Policy rejects disallowed frame/document context   |
| Extension sender | Content script, extension page, external extension, external web page | Each sender class has a separate explicit policy   |
| Message schema   | Missing, extra, wrong-type, nested, oversized values                  | Parser rejects before operation resolution         |
| Operation        | Known read, unknown operation, deprecated version                     | Only current allowlisted operations exist          |
| Resource         | Known ID, arbitrary path, traversal, symlink, race target             | Final opened object remains inside authorized set  |
| Process          | Argument arrays versus shell string                                   | No attacker-controlled shell interpretation        |
| Lifecycle        | Navigation, port reconnect, worker restart, browser restart           | Authorization does not survive its bound context   |
| Replay           | Duplicate nonce and expired message                                   | No second effect                                   |
| Installation     | Writable manifest, writable parent, redirected host path              | Integrity check or ACL prevents substitution       |
| Output           | Oversized response, stderr noise, stdout noise, malformed JSON        | Bounded failure without protocol desynchronization |
| Cleanup          | Remove extension or host registration                                 | Browser can no longer start the host               |

## Hardening order

Fix the highest-leverage boundary first:

1. **Remove unnecessary Native Messaging access.** A capability that is not installed cannot be
   reached through a future extension bug.
2. **Reduce content-script and host reach.** Narrow match patterns, frames, host permissions,
   external callers, and optional permissions.
3. **Validate browser-observed sender context.** Do not accept page-provided origin or tab claims.
4. **Define a versioned operation schema.** Reject unknown keys, wrong types, excessive size, and
   deprecated operations.
5. **Resolve identifiers inside the trusted component.** Page-supplied paths, commands, URLs, and
   device handles are authority injection.
6. **Separate policy from execution.** The native dispatcher authorizes a named operation and
   resource before calling a small implementation.
7. **Avoid shells and ambient administrator identity.** Use fixed executables, argument arrays,
   sanitized environments, controlled working directories, and the least privileged runtime.
8. **Bind high-impact actions to state.** User gesture, selected object, nonce, expiry, and
   approval must describe the same request.
9. **Verify the effect independently.** Record the expected object change and prove that adjacent
   objects remained unchanged.
10. **Protect the installation path.** Host manifests, registry keys, executable paths, update
    packages, and parent directories are part of the security boundary.

## Detection and forensic signals

Useful telemetry exists on both sides of the boundary:

- extension installation source, ID, version, permission changes, and update events;
- `connectNative` / `sendNativeMessage` operation name and request identifier;
- native host process parent, command line, origin argument, UID, executable hash, and signature;
- manifest or registry changes for Native Messaging host registration;
- child processes spawned by the host;
- file, socket, device, credential, or configuration effects attributed to the host process;
- denied operations by gate and reason;
- abnormal message size, parse failure, reconnect rate, and replay detection;
- unexpected host execution when the browser is idle or no approved workflow exists.

Logs should record identifiers and decisions, not secret payloads. Native messages may contain
tokens, file content, account names, or local paths. Evidence collection must not create a second
data-exposure path.

## Reporting the finding

A useful finding title describes the lost boundary:

> A browser content script could cause the Native Messaging host to read an object outside the
> approved resource set because the extension forwarded an attacker-controlled path and the host
> performed no object authorization.

The report should include:

- exact extension and native host versions;
- installation and runtime identity;
- reachable sender contexts;
- minimal request schema;
- approved and unauthorized canary objects;
- positive effect evidence;
- negative controls;
- maximum demonstrated impact without using real sensitive data;
- root cause at both the extension and native host;
- remediation that changes the operation contract, not only a string filter;
- cleanup proof.

Severity follows the reachable effect, not the presence of `nativeMessaging`. A status-only host
may be informational. A user-level host that reads developer credentials may be high impact. A
system service reachable through a broad dispatcher may cross a privilege boundary.

## Final model

Native Messaging is secure when the browser extension and native host behave as one narrow,
versioned authorization protocol:

```text
browser-observed sender
  → allowed operation
  → trusted resource resolution
  → bounded native identity
  → independently verified effect
```

The browser sandbox remains valuable. The extension ID remains valuable. The host manifest remains
valuable. But none of them authorizes an arbitrary native effect.

The bridge is trustworthy only when the original web context survives long enough for the native
side to make a deliberate, testable, and observable decision.