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.
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.
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.
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).
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
- Run the CI checks on a branch that adds a package with
evalin its bundle and confirm the build fails. - Confirm
npm ciis used in every build and the lockfile is committed. - Compare the shipped-dependency list between two releases and confirm every change was reviewed.
- 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.
Related
- Writing a strict content security policy for MV3 — the runtime backstop.
- Keeping the extension bundle small — fewer dependencies, smaller bundles.
- Building reproducible release zips — knowing exactly what you shipped.
- Extension security and CSP hardening — the parent topic.