Series · 3 partsBrowser-to-Native Trust BoundariesPart 1 · You are here
  1. 1The Extension Was Sandboxed. The Native Host Was Not.You are here
  2. 2The Page Never Had Permission. The Extension Did.
  3. 3The Package Was Signed. The Update Was Still a Security Decision.

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

BROWSER → NATIVE AUTHORITY GRAPH ISOLATION CHANGES EXECUTION CONTEXT. IT DOES NOT AUTHORIZE THE NEXT EFFECT. WEB PAGEorigin · frameDOM · user input CONTENT SCRIPTisolated worldLESS TRUSTED SERVICE WORKERextension APIsPOLICY POINT STDIO JSONlength frameSERIALIZED HOSTnative UIDOS access Browser-side facts sender.id · sender.url · tab.id frameId · documentId · gesture LOST IF NOT BOUND TO REQUEST Native request contract version · operation · resource nonce · expiry · approval ALLOWLISTED SCHEMA Operating-system effect file · process · device credential · socket · config INDEPENDENTLY VERIFIED THE HOST MANIFEST SELECTS AN EXTENSION. IT DOES NOT PRESERVE THE ORIGINAL WEB AUTHORIZATION CONTEXT.
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.

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.

MechanismWhat it establishesWhat it does not establish
Extension nativeMessaging permissionThe extension may use the Native Messaging APIThat every extension component should invoke every native operation
Host allowed_origins / allowed_extensionsThe named extension ID may start the hostWhich page, frame, tenant, or user workflow originated the request
Isolated content-script worldPage JavaScript cannot directly read the content script’s variablesThat data derived from the page is trustworthy
sender.id and sender.urlBrowser-observed sender context for a messageThat an operation, path, or object is authorized
Length-prefixed JSONA transport format with message boundariesA safe schema, semantic authorization, replay protection, or resource limits
Native process UIDThe OS identity under which the host executesThat the browser caller should inherit all of that identity’s access
Browser extension reviewA distribution and policy checkpointThe 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:

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:

ClassDangerous designResulting risk
Confused deputyAny content script message is forwarded to the hostA page borrows extension and host authority
Missing object authorizationThe caller supplies an arbitrary path, device ID, profile, or accountCross-user or cross-tenant access
Argument injectionThe host concatenates message fields into a shell or command lineNative code execution under the host identity
Path traversal / link followingA relative path is joined and opened without final-object validationRead or write outside the intended root
ReplayA privileged request has no nonce, expiry, or state bindingA captured message repeats a prior effect
Parser asymmetryBrowser and host coerce types or sizes differentlyValidation occurs on a different object than execution
Lifecycle confusionLong-lived ports retain state across navigation or frame replacementNew page context inherits old authorization
Installation boundary failureA writable manifest or host path redirects the registered binaryLocal 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:

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:

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.

Architectural cutaway showing a web page crossing isolated extension chambers and a framed message conduit before reaching native filesystem and process machinery.
Boundary plate / 01 The browser-to-native path is not one pipe. Each chamber changes principal, available context, and authority.

Inventory before interaction

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

Extension inventory

FieldEvidence
Extension ID and install sourceBrowser extension page, enterprise policy, signed package metadata
Manifest version and update URLmanifest.json and managed-extension policy
Permissionspermissions, optional_permissions, host_permissions
Content-script reachMatch patterns, excluded matches, frames, execution world
External callersexternally_connectable IDs and page patterns
Message listenersonMessage, onConnect, external variants, port names
Native operationsEvery connectNative and sendNativeMessage call site
Sender checkssender.id, URL origin, tab, frame, document, incognito state
State bindingGesture, nonce, expiry, selected object, active tab, tenant

Native host inventory

FieldEvidence
Manifest location and ownerOS path or registry key, ownership and ACL
Host executable pathAbsolute resolved path and final file identity
Allowed extension IDsExact allowed_origins or allowed_extensions values
Runtime identityUID, token, integrity level, sandbox, service account
ParserLength handling, UTF-8 decoding, JSON type checks, maximum size
DispatcherOperation allowlist and default-deny behavior
Object authorizationHow resource IDs become paths, devices, or accounts
Process creationShell use, argument arrays, environment, working directory
Output behaviorData classification, truncation, errors, stderr logging
Update pathInstaller 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.

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:

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.

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

NATIVE EFFECT DECISION PIPELINE VALIDATE CONTEXT BEFORE CREATING A NATIVE CAPABILITY 1 · SENDERorigin · framedocument · gesture 2 · SCHEMAversion · typessize · unknown keys 3 · RESOLVEoperation IDresource ID 4 · POLICYallow · expiryreplay gate 5 · EXECUTEnarrow UIDno shell DENIAL RECORDgate · reason · request IDno operation · no side effect EFFECT VERIFIERexpected object changed?unexpected object unchanged? EVIDENCE BUNDLErequest · decision · effectnegative control · cleanup FAIL CLOSED WHEN: UNKNOWN SENDER · STALE DOCUMENT · EXTRA FIELD · UNKNOWN RESOURCE · REPLAY · AMBIENT ADMIN · EFFECT MISMATCH
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.

Evidence matrix

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

ClaimPositive evidenceNegative controlStrong conclusion
The page can reach the extension listenerBrowser-observed message with tab, frame, document, and originSame message from a non-matching origin is rejectedReachability is limited to the recorded sender set
The extension can start the hostHost process creation correlated to request IDUnlisted extension ID receives an access denialHost manifest filters extension identity
The host exposes an operationValid schema returns the expected canary resultUnknown operation and extra field are rejectedDispatcher is explicit and default-deny
Resource authorization worksAllowed resource ID opens the mapped canaryTraversal, absolute path, symlink, and unknown ID failCaller cannot select an arbitrary object through tested variants
Replay is controlledFresh nonce succeeds onceSame nonce and expired request fail with no effectTested privileged request is single-use within its lifetime
Native authority is boundedRuntime identity and system call trace match expected objectObserver confirms protected and unrelated objects remain untouchedDemonstrated effect is limited to the tested capability
Cleanup is completeLab manifest, extension, canaries, process, and logs are removedReconnection attempt fails after cleanupTest 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

LayerTestExpected secure result
Web originSame message from allowed and unallowed originsOnly the exact allowed origin proceeds
FrameTop frame versus embedded or stale framePolicy rejects disallowed frame/document context
Extension senderContent script, extension page, external extension, external web pageEach sender class has a separate explicit policy
Message schemaMissing, extra, wrong-type, nested, oversized valuesParser rejects before operation resolution
OperationKnown read, unknown operation, deprecated versionOnly current allowlisted operations exist
ResourceKnown ID, arbitrary path, traversal, symlink, race targetFinal opened object remains inside authorized set
ProcessArgument arrays versus shell stringNo attacker-controlled shell interpretation
LifecycleNavigation, port reconnect, worker restart, browser restartAuthorization does not survive its bound context
ReplayDuplicate nonce and expired messageNo second effect
InstallationWritable manifest, writable parent, redirected host pathIntegrity check or ACL prevents substitution
OutputOversized response, stderr noise, stdout noise, malformed JSONBounded failure without protocol desynchronization
CleanupRemove extension or host registrationBrowser 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:

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.

Sources & freshness

How current is this note?

Sources checkedAugust 31, 2026

The latest source-review, content-update, or publication date is shown.

ReviewPublic sources reviewed

Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.