Command Palette

Search for a command to run...

Hectal
Lab 1.1·Supply ChainCRITICAL

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
reading advisoriesnpm audit and osv-scannerautomated dependency updatesschema validation as defence in depth

Run these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.

Lab setup

  1. 01The app and its dependencies

    A small Express app whose PATCH /profile handler 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

  1. 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 to Object.prototype itself, 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 own isAdmin can suddenly read an inherited value. Depending on the code, pollution leads to authorisation bypass, denial of service, and in some libraries remote code execution.

  2. 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

  1. 01npm audit

    npm audit compares the lockfile against the GitHub Advisory Database. --audit-level=high makes 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 report
     
    lodash <=4.17.20
    Severity: critical
    Prototype Pollution in lodash
    Command Injection in lodash
    Regular Expression Denial of Service (ReDoS) in lodash
    fix available via `npm audit fix --force`
    Will install lodash@4.17.21, which is outside the stated dependency range
    node_modules/lodash
     
    1 critical severity vulnerability
  2. 02osv-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 /src
    Scanned /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

  1. 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"
  2. 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);
    });
  3. 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

  1. 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 400
    2 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

01

Which command shows WHY lodash is in your tree: which of your direct dependencies pulls it in?

02

What's the difference between npm install and npm ci in CI?

Interview questions

01

How do you manage vulnerable dependencies across many services?

02

What is prototype pollution?

0/4 · 0%