---
title: "The Package Was Signed. The Update Was Still a Security Decision."
description: "A browser extension update preserves publisher identity while it can change code, permissions, remote configuration, and privileged behavior across an entire installed fleet. This research method turns that release path into an evidence-backed authorization boundary."
date: 2026-08-31
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/browser-extension-update-supply-chain/
---

## The boundary in 60 seconds

A valid extension signature answers a narrow question: **did this package come from the identity the
browser expects for this extension?** It does not prove that the release was produced from the
reviewed commit, that the build runner was clean, that the publisher account intended this upload,
that permissions did not expand, or that remote configuration cannot activate a new capability.

Chrome automatically checks for extension updates on startup and every few hours. An available
Manifest V3 update is installed after the extension becomes idle. Managed environments may pin a
version or override its update source. Firefox-listed add-ons update through AMO; self-distributed
add-ons may use an HTTPS update manifest and still require Mozilla signing for release and beta
Firefox.

Those controls establish distribution mechanics. The security decision remains:

> Which reviewed source, build identity, artifact digest, publisher action, permission set, rollout
> cohort, and runtime configuration produced the code now exercising extension authority?

This final part of the series builds a method for answering that question without publishing a
malicious extension or touching a real user fleet.

## One release, seven principals

<figure class="diagram">
<svg viewBox="0 0 780 448" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Extension release authority graph from source commit through CI, artifact, publisher, update service, browser installation, and runtime configuration.">
<defs><marker id="beu-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="beu-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">EXTENSION RELEASE AUTHORITY GRAPH</text><text x="4" y="48" class="dg-muted">SIGNER CONTINUITY IS NOT RELEASE AUTHORIZATION</text>
<rect x="0" y="82" width="102" height="88" rx="8" class="dg-box"/><text x="14" y="108" class="dg-label">SOURCE</text><text x="14" y="134" class="dg-mono">commit · review</text><text x="14" y="156" class="dg-muted">INTENT</text>
<line x1="104" y1="126" x2="126" y2="126" class="dg-line-accent" marker-end="url(#beu-a)"/><rect x="130" y="82" width="102" height="88" rx="8" class="dg-box"/><text x="144" y="108" class="dg-label">BUILD</text><text x="144" y="134" class="dg-mono">runner · deps</text><text x="144" y="156" class="dg-muted">TRANSFORM</text>
<line x1="234" y1="126" x2="256" y2="126" class="dg-line-accent" marker-end="url(#beu-a)"/><rect x="260" y="82" width="102" height="88" rx="8" class="dg-box-accent"/><text x="274" y="108" class="dg-label">ARTIFACT</text><text x="274" y="134" class="dg-mono">digest · SBOM</text><text x="274" y="156" class="dg-accent">EVIDENCE</text>
<line x1="364" y1="126" x2="386" y2="126" class="dg-line-crit" marker-end="url(#beu-r)"/><rect x="390" y="82" width="102" height="88" rx="8" class="dg-box-crit"/><text x="404" y="108" class="dg-crit">PUBLISHER</text><text x="404" y="134" class="dg-mono">account · token</text><text x="404" y="156" class="dg-muted">UPLOAD</text>
<line x1="494" y1="126" x2="516" y2="126" class="dg-line-accent" marker-end="url(#beu-a)"/><rect x="520" y="82" width="112" height="88" rx="8" class="dg-box"/><text x="534" y="108" class="dg-label">UPDATE</text><text x="534" y="134" class="dg-mono">store · URL</text><text x="534" y="156" class="dg-muted">DELIVERY</text>
<line x1="634" y1="126" x2="656" y2="126" class="dg-line-accent" marker-end="url(#beu-a)"/><rect x="660" y="82" width="120" height="88" rx="8" class="dg-box-crit"/><text x="674" y="108" class="dg-crit">BROWSER</text><text x="674" y="134" class="dg-mono">installed fleet</text><text x="674" y="156" class="dg-muted">EFFECT</text>
<rect x="36" y="228" width="214" height="122" rx="8" class="dg-box"/><text x="54" y="256" class="dg-label">Identity facts</text><text x="54" y="284" class="dg-mono">commit signer · workflow ID</text><text x="54" y="308" class="dg-mono">publisher actor · store item</text><text x="54" y="332" class="dg-muted">WHO MOVED THE RELEASE?</text>
<rect x="283" y="228" width="214" height="122" rx="8" class="dg-box-accent"/><text x="301" y="256" class="dg-label">Artifact facts</text><text x="301" y="284" class="dg-mono">digest · manifest · permissions</text><text x="301" y="308" class="dg-mono">dependency lock · build recipe</text><text x="301" y="332" class="dg-accent">WHAT WAS DELIVERED?</text>
<rect x="530" y="228" width="214" height="122" rx="8" class="dg-box-crit"/><text x="548" y="256" class="dg-label">Runtime facts</text><text x="548" y="284" class="dg-mono">cohort · effective config</text><text x="548" y="308" class="dg-mono">permission · privileged effect</text><text x="548" y="332" class="dg-crit">WHAT BECAME POSSIBLE?</text>
<line x1="144" y1="352" x2="144" y2="398" class="dg-line"/><line x1="390" y1="352" x2="390" y2="398" class="dg-line"/><line x1="638" y1="352" x2="638" y2="398" class="dg-line"/><line x1="144" y1="398" x2="638" y2="398" class="dg-line-accent" marker-end="url(#beu-a)"/><text x="246" y="430" class="dg-accent">RELEASE AUTHORIZATION = IDENTITY + ARTIFACT + RUNTIME DELTA</text>
</svg>
<figcaption><strong>Figure 1.</strong> Every handoff can preserve a valid extension identity while changing the evidence needed to authorize the release.</figcaption>
</figure>

| Principal             | Security decision                             | Evidence that must survive                                    |
| --------------------- | --------------------------------------------- | ------------------------------------------------------------- |
| Source reviewer       | Is this change intended and bounded?          | Commit, review, changed files, threat note                    |
| Build service         | Did the approved source produce this package? | Workflow identity, clean runner, dependency lock, logs        |
| Artifact store        | Is this the exact reviewed output?            | SHA-256 digest, manifest snapshot, SBOM, provenance statement |
| Publisher account     | Was this upload authorized?                   | Strong authentication, actor, timestamp, artifact digest      |
| Store or update host  | Which version is offered to which client?     | Version mapping, update URL, rollout record                   |
| Browser               | Which package is actually installed?          | Extension ID, version, permissions, update source             |
| Runtime control plane | Which behavior is active now?                 | Effective config, cohort, expiry, audit event                 |

## What the platform proves—and what it does not

| Platform control          | Establishes                                                            | Does not establish                                                    |
| ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |
| Package signature         | Continuity with the expected extension identity                        | The release is benign or matches reviewed source                      |
| Store review              | Submitted package passed the store's current process                   | Publisher credentials, backend config, or future releases remain safe |
| Manifest V3 packaged code | Extension logic is bundled rather than fetched as ordinary remote code | Remote data cannot act as a hidden instruction language               |
| Permission warning        | A declared permission delta may be visible or block activation         | Existing broad permissions are used only for the old purpose          |
| HTTPS update manifest     | Network delivery has server authentication and integrity in transit    | The update origin and publisher pipeline are correctly authorized     |
| Version pin               | A cohort remains on one version                                        | The pinned version is safe or receives a critical fix                 |
| Staged rollout            | Exposure can be limited over time                                      | The selected cohort is observed or rollback is complete               |

The distinction matters because an extension update is not merely new code. It is a fleet-wide
change to a principal that may already hold `tabs`, `cookies`, `storage`, `downloads`, host access,
or a native messaging bridge.

## The five failure families

| Failure family        | Boundary lost                                                      | Typical observable result                                    |
| --------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ |
| Publisher takeover    | Store account or upload token becomes sufficient release authority | Validly distributed unauthorized package                     |
| Build substitution    | CI output is not bound to reviewed source                          | Signed artifact contains unreviewed code or dependencies     |
| Permission drift      | Review considers code but not effective capability delta           | Update gains broader host or browser access                  |
| Configuration-as-code | Remote JSON selects operations rather than bounded data            | Store-reviewed package activates unreviewed behavior         |
| Rollout blindness     | Release success is inferred from upload completion                 | Vulnerable or unauthorized versions remain active in cohorts |

Remote hosted code restrictions are an important control, but the research question is broader.
A remote response does not need to contain JavaScript to become logic. A configuration grammar
with arbitrary operation names, URLs, selectors, scripts, or rule expressions can transfer the
security decision out of the reviewed package.

## A bounded synthetic release lab

The lab needs no store upload and no real user. Use a disposable extension ID, a local mock update
service, two inert packages, and one canary capability: incrementing a counter in extension-local
storage. Version `1.0.0` may increment only `release_a`; version `1.0.1` is tested against permission,
artifact, configuration, and rollout gates before it may increment `release_b`.

<figure class="evidence">
  <picture>
    <source type="image/webp" srcset="/images/posts/responsive/browser-extension-release-provenance-768.webp 768w, /images/posts/responsive/browser-extension-release-provenance-1200.webp 1200w, /images/posts/browser-extension-release-provenance.webp 1536w" sizes="(max-width: 760px) 92vw, 760px" />
    <img src="/images/posts/browser-extension-release-provenance.webp" alt="Parallel extension release pipelines comparing provenance-linked delivery with a validly signed but authorization-drifting release." loading="lazy" decoding="async" width="1536" height="1024" />
  </picture>
  <figcaption><strong>Evidence plate 01.</strong> The lower path preserves distribution identity while runtime authority diverges through one unmeasured control point.</figcaption>
</figure>

The positive test proves only that the authorized package and cohort can change the intended
canary. Negative controls must show that a different digest, unexpected permission, expired config,
wrong rollout cohort, and replayed approval create no state change.

## Build an artifact contract

Record release facts next to the package rather than reconstructing them after an incident.

```json
{
  "extension_id": "lab-extension",
  "version": "1.0.1",
  "source_commit": "8d6d...e21",
  "workflow_identity": "release.yml@refs/tags/v1.0.1",
  "artifact_sha256": "7f4a...91c",
  "manifest_sha256": "c1e3...a42",
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://lab.invalid/*"],
  "sbom_sha256": "0bd2...723",
  "built_at": "2026-08-31T07:00:00Z"
}
```

The contract is useful only if the publisher verifies it against the artifact it uploads. A digest
created after an uncontrolled upload is documentation, not authorization.

## Treat manifest changes as security changes

The manifest is the extension's declared authority surface. Diff it separately from application
code and fail closed on unexpected additions.

```js
const authority = (manifest) => ({
  permissions: [...(manifest.permissions ?? [])].sort(),
  optional_permissions: [...(manifest.optional_permissions ?? [])].sort(),
  host_permissions: [...(manifest.host_permissions ?? [])].sort(),
  content_scripts: (manifest.content_scripts ?? []).map(
    ({ matches, all_frames, world }) => ({
      matches: [...matches].sort(),
      all_frames: Boolean(all_frames),
      world: world ?? "ISOLATED",
    }),
  ),
  externally_connectable: manifest.externally_connectable ?? null,
  update_url: manifest.update_url ?? null,
});
```

Compare the normalized result with the previous production release. New host patterns, `all_frames`,
external callers, native messaging, debugger access, or an update-source change require a named
reviewer and an explicit rationale. A version bump is not that rationale.

## Keep remote configuration below the policy line

Safer remote configuration selects among packaged, bounded behaviors:

```json
{
  "schema": 2,
  "issued_at": "2026-08-31T07:00:00Z",
  "expires_at": "2026-08-31T08:00:00Z",
  "cohort": "canary-05",
  "flags": { "new_panel": false },
  "limits": { "batch_size": 20 }
}
```

High-risk configuration accepts a dispatcher, URL, expression, selector, or script-like data:

```json
{
  "operation": "run",
  "target": "https://runtime-selected.example/*",
  "steps": [{ "action": "inject", "payload": "server-controlled" }]
}
```

Reject unknown keys, enforce a small schema, bind the document to a release version and cohort,
verify expiry, and default to the last known safe packaged behavior. Configuration signatures can
authenticate a control plane; they do not make an overpowered grammar safe.

## Rollout is a state machine, not a percentage

<figure class="diagram">
<svg viewBox="0 0 780 366" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fail-closed extension rollout state machine from candidate through canary, staged, production, rollback, and quarantine.">
<defs><marker id="beu-s-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="beu-s-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">FAIL-CLOSED RELEASE STATE MACHINE</text><text x="4" y="48" class="dg-muted">PROMOTION REQUIRES NEW EVIDENCE; TIME DOES NOT PROMOTE A RELEASE</text>
<rect x="0" y="92" width="130" height="78" rx="8" class="dg-box"/><text x="18" y="122" class="dg-label">CANDIDATE</text><text x="18" y="148" class="dg-mono">digest bound</text><line x1="132" y1="131" x2="170" y2="131" class="dg-line-accent" marker-end="url(#beu-s-a)"/>
<rect x="174" y="92" width="130" height="78" rx="8" class="dg-box-accent"/><text x="192" y="122" class="dg-label">CANARY</text><text x="192" y="148" class="dg-mono">5% observed</text><line x1="306" y1="131" x2="344" y2="131" class="dg-line-accent" marker-end="url(#beu-s-a)"/>
<rect x="348" y="92" width="130" height="78" rx="8" class="dg-box"/><text x="366" y="122" class="dg-label">STAGED</text><text x="366" y="148" class="dg-mono">25% · 50%</text><line x1="480" y1="131" x2="518" y2="131" class="dg-line-accent" marker-end="url(#beu-s-a)"/>
<rect x="522" y="92" width="130" height="78" rx="8" class="dg-box-accent"/><text x="540" y="122" class="dg-label">PRODUCTION</text><text x="540" y="148" class="dg-mono">version proven</text>
<rect x="174" y="238" width="130" height="78" rx="8" class="dg-box-crit"/><text x="192" y="268" class="dg-crit">QUARANTINE</text><text x="192" y="294" class="dg-mono">halt update</text><rect x="522" y="238" width="130" height="78" rx="8" class="dg-box-crit"/><text x="540" y="268" class="dg-crit">ROLLBACK</text><text x="540" y="294" class="dg-mono">known-safe build</text>
<path d="M239 172 V232" class="dg-line-crit" marker-end="url(#beu-s-r)"/><path d="M413 172 C413 214 239 208 239 232" class="dg-line-crit" marker-end="url(#beu-s-r)"/><path d="M587 172 V232" class="dg-line-crit" marker-end="url(#beu-s-r)"/><path d="M520 277 H308" class="dg-line-crit" marker-end="url(#beu-s-r)"/>
<text x="328" y="348" class="dg-muted">DENIAL · CRASH · PERMISSION DRIFT · UNKNOWN DIGEST → STOP</text>
</svg>
<figcaption><strong>Figure 2.</strong> Promotion is an authorization event backed by cohort evidence; rollback must also identify the artifact and effective configuration.</figcaption>
</figure>

## Reproduction methodology

1. Inventory release identities, update sources, store roles, CI credentials, and runtime config.
2. Build the same tagged source twice in isolated runners and compare outputs or explain every
   nondeterministic byte.
3. Capture the unpacked manifest and package digest before upload.
4. Test an unexpected permission and host pattern; the release gate must reject both.
5. Offer an authorized package only to a synthetic canary cohort.
6. Replay the approval with a different digest; it must not publish or promote.
7. Serve expired, oversized, unknown-key, wrong-cohort, and wrong-version configuration.
8. Prove rollback removes the tested effect and identify clients that remain on the old version.

## Test matrix

| Dimension     | Variants                                            | Secure result                                    |
| ------------- | --------------------------------------------------- | ------------------------------------------------ |
| Source        | Reviewed tag, moved tag, unreviewed commit          | Only immutable reviewed source builds            |
| Build         | Clean runner, altered dependency, reused workspace  | Artifact or provenance delta blocks release      |
| Artifact      | Expected digest, repackaged ZIP, manifest drift     | Only exact authorized digest proceeds            |
| Publisher     | Human MFA, scoped workload identity, stale token    | Short-lived scoped identity; stale token denied  |
| Permissions   | Same, added optional, broad host, native bridge     | Any authority expansion requires explicit review |
| Update source | Store, approved enterprise URL, unexpected override | Inventory matches policy and installed state     |
| Configuration | Valid, expired, replayed, unknown key, wrong cohort | Invalid documents create no privileged effect    |
| Rollout       | Canary, staged, pinned, offline, rollback           | Every installed cohort is observable and bounded |

## Evidence matrix

| Claim                                | Positive evidence                                                   | Negative control                                                          | Strong conclusion                                               |
| ------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Reviewed source produced the package | Commit, workflow identity, reproducible output, matching digest     | Moved tag or changed dependency produces a different or rejected artifact | Tested artifact is bound to reviewed source and build context   |
| Upload was authorized                | Publisher actor and exact digest recorded                           | Same approval with another digest is denied                               | Approval cannot authorize an arbitrary package                  |
| Authority did not drift              | Normalized manifest equals approved capability set                  | Added host or permission blocks promotion                                 | Tested release cannot silently expand declared authority        |
| Remote config is data                | Valid bounded schema changes only documented flags                  | Operation, URL, unknown key, expiry, and replay are rejected              | Tested control plane cannot select arbitrary privileged actions |
| Canary limits exposure               | Only synthetic cohort receives candidate and reports version/digest | Non-canary client remains on production                                   | Tested rollout selector is enforced                             |
| Rollback is complete                 | Known-safe version and config replace candidate                     | Candidate effect and digest disappear from every observed cohort          | Tested fleet returned to the recorded safe state                |

## Hardening order

1. Require phishing-resistant MFA for store owners and remove unused publisher roles.
2. Replace long-lived upload secrets with short-lived, repository-bound workload identity where the
   store supports it.
3. Build from immutable tags in isolated runners; pin actions and dependencies.
4. Generate package digest, manifest snapshot, SBOM, and provenance before upload.
5. Make the publisher accept only an approved digest and version.
6. Gate manifest authority changes separately from code review.
7. Keep remote config typed, bounded, expiring, cohort-bound, and incapable of selecting code-like
   operations.
8. Measure installed versions and effective configuration by cohort.
9. Rehearse both store rollback and kill-switch behavior without depending on the compromised
   publisher path.

## Detection and incident evidence

Preserve store audit events, role changes, failed MFA, CI workflow identity, dependency-lock changes,
artifact digests, manifest authority diffs, update-source policy, installed versions, configuration
hashes, and privileged operation summaries. Never log cookies, page contents, tokens, or full URLs
when a stable origin or operation identifier is sufficient.

```json
{
  "event": "extension_release_observed",
  "extension_id": "lab-extension",
  "version": "1.0.1",
  "artifact_sha256": "7f4a...91c",
  "manifest_policy": "approved",
  "cohort": "canary-05",
  "config_sha256": "2f91...bb0",
  "decision": "promote-denied",
  "reason": "unexpected-host-permission"
}
```

## Final model

The package signature is necessary evidence, not the final verdict.

```text
TRUSTED RELEASE
= expected publisher identity
+ reviewed immutable source
+ controlled build identity
+ authorized artifact digest
+ reviewed authority delta
+ bounded runtime configuration
+ observable rollout and rollback
```

Part 1 followed a message into a native process. Part 2 followed a page into extension authority.
Part 3 closes the loop by proving how the privileged extension code reached the browser in the first
place. The complete series has one rule: **reachability, identity, signing, and delivery are inputs
to authorization; none is authorization by itself.**