A four-year-old lodash in the lockfile
package-lock.json pins a lodash version with a published prototype-pollution vulnerability, used on a code path that deep-merges request bodies.
- Weakness
- CWE-1321 · Prototype pollution · CVE-2018-3721
- Target
sec-lab/app— Express + lodash 4.17.4
npm audit and osv-scannerautomated dependency updatesschema validation as defence in depthRun these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.
Lab setup
01The app and its dependencies
A small Express app whose
PATCH /profilehandler deep-merges the JSON body into the user's settings with_.merge, a very common pattern. lodash is pinned to an old version, as it is in plenty of apps nobody has touched in years.sec-lab/app/package.jsonwhole filejson { "name": "sec-lab-app", "version": "1.0.0", "dependencies": { "express": "^4.21.2", "lodash": "4.17.4" } }
The threat
01What prototype pollution is
In JavaScript, every plain object inherits from
Object.prototype. Vulnerable deep-merge functions don't guard against special keys like__proto__in untrusted input, so a crafted request can add properties toObject.prototypeitself, and those properties then appear on EVERY object in the process.Any later check of the form
if (user.isAdmin)on an object that doesn't define its ownisAdmincan suddenly read an inherited value. Depending on the code, pollution leads to authorisation bypass, denial of service, and in some libraries remote code execution.02Why this one is serious here
The vulnerable function receives unauthenticated user input on a hot path, and the advisory has been public since 2018, so automated scanners and attackers both know exactly which versions are affected. 'Known, public, reachable' is the most urgent class of dependency risk.
What's at risk
- Potential authorisation bypass for any user who can reach the endpoint.
- Process-wide state corruption, which is hard to diagnose and can crash the service.
Detect
01npm audit
npm auditcompares the lockfile against the GitHub Advisory Database.--audit-level=highmakes it exit non-zero for high and critical findings, which makes it CI-ready.terminal$ cd sec-lab/app && npm audit --audit-level=high── output ──# npm audit reportlodash <=4.17.20Severity: criticalPrototype Pollution in lodashCommand Injection in lodashRegular Expression Denial of Service (ReDoS) in lodashfix available via `npm audit fix --force`Will install lodash@4.17.21, which is outside the stated dependency rangenode_modules/lodash1 critical severity vulnerability02osv-scanner: one tool across ecosystems
Google's OSV-Scanner reads lockfiles for npm, pip, Go, Maven, Cargo and more, and queries osv.dev. Useful in a polyglot repo, and it can scan container images too.
terminal$ docker run --rm -v "$PWD:/src" ghcr.io/google/osv-scanner:v2.0.2 scan source -r /src── output ──Scanning dir /srcScanned /src/package-lock.json file and found 66 packages╭─────────────────────────────────────┬──────┬───────────┬─────────┬─────────┬───────────────────╮│ OSV URL │ CVSS │ ECOSYSTEM │ PACKAGE │ VERSION │ SOURCE │├─────────────────────────────────────┼──────┼───────────┼─────────┼─────────┼───────────────────┤│ https://osv.dev/GHSA-fvqr-27wr-82fm │ 6.5 │ npm │ lodash │ 4.17.4 │ package-lock.json ││ https://osv.dev/GHSA-35jh-r3h4-6jhm │ 7.2 │ npm │ lodash │ 4.17.4 │ package-lock.json ││ ... │ │ │ │ │ │╰─────────────────────────────────────┴──────┴───────────┴─────────┴─────────┴───────────────────╯
Defend
011. Upgrade — and keep upgrading automatically
The fix is one version bump. The lasting fix is never falling four years behind again: Dependabot or Renovate opens pull requests for updates, grouped so they're easy to review, and CI (tests plus
npm audit) decides whether they're safe to merge.Vulnerable
package.jsonadd to filejson "lodash": "4.17.4"Hardened
.github/dependabot.ymlwhole fileyaml version: 2 updates: - package-ecosystem: npm directory: /app schedule: { interval: weekly } groups: minor-and-patch: update-types: [minor, patch] # package.json now: "lodash": "^4.17.21"022. Defence in depth: validate input shape at the boundary
Even with a patched library, deep-merging arbitrary JSON into server state is risky. Validate the body against a strict schema first: only known keys, known types. zod's
.strict()rejects unexpected keys, and unexpected keys include the special ones pollution relies on. Merge only the validated object.Vulnerable
server.jsadd to filejs app.patch("/profile", (req, res) => { _.merge(user.settings, req.body); res.json(user.settings); });Hardened
server.jsadd to filejs const { z } = require("zod"); const Settings = z.object({ theme: z.enum(["light", "dark"]).optional(), newsletter: z.boolean().optional(), }).strict(); app.patch("/profile", (req, res) => { const parsed = Settings.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: "invalid settings" }); Object.assign(user.settings, parsed.data); res.json(user.settings); });033. Gate the pipeline
Fail the build on high or critical findings in the lockfile, with a documented way to accept a risk temporarily (an ignore entry with an expiry date and a ticket), so the gate stays trusted instead of being disabled.
Hardened
.github/workflows/ci.ymladd to fileyaml - run: npm ci - run: npm audit --audit-level=high - uses: google/osv-scanner-action/osv-scanner-action@<sha> # v2 with: scan-args: --recursive ./
Verify
01Scanners are clean, and the validation test passes
Add a unit test that sends a body with an unexpected key and asserts a 400. It documents the rule and protects it from future refactors.
terminal$ npm install lodash@^4.17.21 zod && npm audit --audit-level=high && npm test── output ──found 0 vulnerabilities✔ PATCH /profile accepts known settings✔ PATCH /profile rejects unexpected keys with 4002 passing
The concepts
Known-vulnerable components (OWASP A06)
Most exploited vulnerabilities are old and public. Attackers scan for version strings and fingerprints, and exploit code is often published alongside advisories. The defence isn't heroic: know what you run (lockfiles, SBOMs), learn about advisories automatically, and keep dependencies moving so a fix is always a small bump rather than a risky multi-year upgrade.
Reachability and prioritisation
Scanners report every vulnerable package, including ones your code never calls. Prioritise by severity, whether a fix exists, whether the vulnerable function is reachable from untrusted input, and whether it's being exploited in the wild (CISA's KEV catalogue, EPSS scores). Then patch the rest on a regular cadence anyway, since reachability analysis is never perfect.
Your turn
Which command shows WHY lodash is in your tree: which of your direct dependencies pulls it in?
What's the difference between npm install and npm ci in CI?
Interview questions
How do you manage vulnerable dependencies across many services?
What is prototype pollution?