Auditing Third-Party Dependencies in an Extension

Reduce supply-chain risk in an MV3 extension: inventory what ships, npm audit and lockfiles, checking dependencies for eval and remote code, pinning, reviewing updates, minimising content-script dependencies and SBOMs.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

An extension’s real permission set is the union of what its own code and every bundled dependency can do. A date library compromised in a minor release runs with your host permissions on every site your content script matches; an analytics helper that loads a remote script turns your extension into a store-policy violation; a transitive dependency using eval breaks under the MV3 CSP only on the code path you did not test. Extensions are attractive targets for supply-chain attacks precisely because they run with privileges on millions of pages. This guide sets up an audit process proportionate to that risk. It belongs to extension security and CSP hardening.

Why extension dependencies carry more risk

A dependency in a web app runs with that app’s origin; a dependency in an extension runs with the extension’s capabilities — in the service worker, with access to every chrome.* API you requested; in content scripts, inside every matching page. Updates reach users automatically through the store within hours, so a malicious version published upstream and pulled into your next release ships quickly and widely. Store review offers partial protection at best: reviewers look for remote code and obvious abuse, not for a subtle exfiltration hidden in a minified bundle. The defences are an accurate inventory of what ships, scrutiny proportional to where each dependency runs, slow and deliberate updates, and as few dependencies as possible in the most exposed contexts.

Where a dependency runs and what it can reachDependencies bundled into content scripts run inside every matching page, those in the service worker reach every granted extension API, those in extension pages reach the page's APIs, and build-only dependencies never ship.Content scriptsevery matching pagehighest exposureService workerall granted chrome.* APIshighest privilegeExtension pagespopup, options, paneluser data + APIsBuild-onlybundler, linters, testsnever shipped
Scrutinise dependencies by the context they end up in.

Step-by-step: an audit you can repeat each release

1. Inventory what actually ships

1# Production dependency tree only
2npm ls --omit=dev --all > deps-prod.txt
3
4# What ended up in each bundle, with sizes
5npx esbuild src/background/sw.ts --bundle --metafile=meta-sw.json --outfile=/dev/null
6npx esbuild src/content/article.ts --bundle --metafile=meta-cs.json --outfile=/dev/null
7node -e 'for (const f of ["meta-sw.json","meta-cs.json"]) { const m=require("./"+f); console.log(f, Object.keys(m.inputs).filter(p=>p.includes("node_modules")).map(p=>p.split("node_modules/")[1].split("/").slice(0,2).join("/")).filter((v,i,a)=>a.indexOf(v)===i)); }'

Execution context: a terminal or CI. The dependency tree lists what you installed; the bundle metafiles list what actually ships in each context. The second list is the one that matters — tree-shaking may drop most of a package, and a single import in a content script pulls a library into every page. Record both per release.

2. Run vulnerability and policy checks

1npm audit --omit=dev --audit-level=high
2npx lockfile-lint --path package-lock.json --allowed-hosts npm --validate-https

Execution context: CI on every pull request. npm audit flags known vulnerabilities in production dependencies; treat high-severity findings in shipped code as release blockers. lockfile-lint ensures every resolved package comes from the registry over HTTPS, catching lockfiles tampered to pull from elsewhere. Commit the lockfile and install with npm ci so builds use exactly the reviewed versions.

Dependency checks in CIOn each pull request, CI installs from the lockfile, audits production dependencies, validates lockfile sources, scans bundles for eval and remote script patterns, and diffs the shipped dependency list against the last release.npm ciexact lockfilenpm auditprod, high+lockfile-lintregistry + httpsthen inspect the bundlesScan bundleseval, remote scriptsDiff shipped depsvs last releaseReview new depshuman sign-off
Every release answers: what ships, is it known-bad, and did anything new appear?

3. Scan bundles for MV3 violations

1for f in dist/chrome/*.js dist/chrome/**/*.js; do
2  grep -HnE "\beval\(|new Function\(|document\.write\(|<script[^>]+src=[\"']https?:" "$f" && FOUND=1
3done
4[ -z "$FOUND" ] || { echo "Potential remote/dynamic code in bundle"; exit 1; }

Execution context: CI after the build. Dependencies that evaluate strings fail under the extension CSP at runtime — often only on a rarely used code path — and dependencies that inject remote scripts violate store policy. Scanning the built output catches both, including in transitive dependencies you never read. Allow-list known false positives explicitly with a comment explaining why. See replacing remotely hosted code.

4. Review every new or updated shipped dependency

1git diff origin/main -- package-lock.json | grep -E '^\+\s+"(node_modules/[^"]+)"' | sed 's/.*node_modules\///; s/".*//' | sort -u

Execution context: code review. A list of packages added or changed in the lockfile turns “bump dependencies” from a rubber stamp into a short review: who maintains each new package, how widely is it used, what does it do on install, does it need network access at runtime? For anything that ends up in a content script or the worker, read the changelog and skim the diff of the published package (npm diff --diff=pkg@old --diff=pkg@new).

Review depth by where a dependency shipsRecommended review effort for dependencies that ship in content scripts, the service worker, extension pages, or only at build time.Ships inNew dependencyUpdateContent scriptJustify + read codeRead diffService workerJustify + read codeRead diffExtension pagesJustify + auditChangelog + auditBuild onlyAuditAudit
Spend review time where the privileges are.

5. Minimise dependencies in content scripts

1// Instead of a 70 KB date library in a content script…
2// import { formatDistanceToNow } from "date-fns";
3const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
4const ago = (ms) => rtf.format(-Math.round((Date.now() - ms) / 60_000), "minute");

Execution context: a content script. Every dependency in a content script runs in every matching page, adds parse time to page loads, and expands the attack surface inside pages you do not control. Web platform APIs — Intl, URL, structuredClone, fetch — replace many small utility packages. Move heavy logic to the worker and keep content scripts thin.

6. Pin and update deliberately

 1// package.json
 2{
 3  "dependencies": {
 4    "dompurify": "3.1.6",            // exact versions for shipped code
 5    "idb": "8.0.0"
 6  },
 7  "devDependencies": {
 8    "vite": "^5.4.0"                 // ranges are acceptable for build tooling
 9  }
10}

Execution context: the package manifest. Exact versions for shipped dependencies mean nothing changes without a lockfile diff someone reviewed. Automated update tools are useful for surfacing updates; configure them to open individual pull requests for shipped dependencies so each gets the review in step 4, rather than batching dozens of bumps into one.

7. Keep a bill of materials per release

1npx @cyclonedx/cyclonedx-npm --omit dev --output-file sbom-$(node -p 'require("./package.json").version').json

Execution context: CI at release time. A software bill of materials records exactly which package versions shipped in each release. When a vulnerability is announced, you can answer “were we affected, in which versions, in which context?” in minutes, and decide whether to ship a fix or a rollback.

Common mistakes

  • Auditing the dependency tree, not the bundles. What ships is what matters.
  • Unreviewed bulk dependency bumps. One malicious update hides among twenty harmless ones.
  • Heavy libraries in content scripts. Exposure and page cost multiply across every site.
  • Ignoring transitive eval. It surfaces as a CSP error on an untested path.
  • No record of what shipped. You cannot assess an advisory without it.

Cross-browser variation

  • Chrome / Edge: store review checks for remote code and obfuscation; minified code is accepted but must not be obfuscated.
  • Firefox: AMO reviewers may request your source and build instructions for bundled code and check third-party libraries against known versions — unmodified, well-known library versions speed up review.
  • Safari: App Review evaluates the whole app; the same supply-chain discipline applies to the web extension resources.

Verification

  1. Run the CI checks on a branch that adds a package with eval in its bundle and confirm the build fails.
  2. Confirm npm ci is used in every build and the lockfile is committed.
  3. Compare the shipped-dependency list between two releases and confirm every change was reviewed.
  4. Locate the SBOM for the current store version.

FAQ

Does minification hide malicious code from me as well as from reviewers?

Yes — which is why reviewing the published package source before bundling matters more than reading your own bundle.

Should I vendor dependencies?

For small, stable ones in content scripts, vendoring a reviewed copy can reduce update churn. Track its origin and version in the SBOM.

What about CDN-hosted libraries in extension pages?

They are remote code and blocked by the MV3 CSP. Bundle them.

Other MV3 Architecture & Extension Lifecycle Resources