In the previous lesson we published @escena-viva/format and saw that, in doing so, we took on a commitment: other people were going to run our code on their machines. This lesson turns the argument around. Every time you type npm install, you are downloading strangers' code and running it with your permissions: the same ones your user has to read your ~/.ssh, your .env or your continuous integration deployment token.

This is not a theoretical warning. The software supply chain is today one of the most profitable attack vectors, precisely because a single compromised package reaches thousands of organizations without any of them having made a mistake. Escena Viva has, as of today, two direct dependencies and a handful of transitive ones: it is the ideal moment to learn to audit them, because the tree still fits in your head. In module 6 we will install Express and the number will multiply.

Contents

  1. The supply chain as an attack surface
  2. npm audit: really reading the output
  3. npm audit fix and the danger of --force
  4. Finding the culprit: npm ls and overrides
  5. Attacks you need to recognize
  6. Practical defensive measures
  7. Continuous maintenance: npm outdated and automation
  8. Licenses: the risk that is not technical
  9. Auditing Escena Viva and writing the routine
  10. Common Mistakes and Tips
  11. Exercises
  12. Conclusion (closing Module 5)

  1. The supply chain as an attack surface

The arithmetic of the npm ecosystem is the root of the problem. A project with 20 direct dependencies usually ends up with between 300 and 800 packages in node_modules, maintained by hundreds of different people nobody has vetted. npm ls --all --parseable | wc -l tells you how many there really are in your project, and the figure is usually a surprise.

Each one of those packages can, during installation or at run time:

  • Read any file your user has access to, including private keys and .env files.
  • Open network connections and send whatever it has read to any server.
  • Run arbitrary commands through the preinstall, install and postinstall hooks we saw in 05-04.

That last point is the most serious and the least known: you do not even have to import the package. A malicious postinstall runs during npm install itself.

There are two ideas worth being clear about from the start:

Common belief Reality
"I only install popular packages" Popularity protects against poor quality, not against an account being hijacked
"It is a development dependency, it never reaches production" It runs on your laptop and in your CI, where the tokens are
"I review the direct dependencies" 95% of the installed code is transitive
"The lock protects me" It protects you from unexpected changes, not from the pinned version being vulnerable

  1. npm audit: really reading the output

npm audit compares the resolved dependency tree (the one in package-lock.json) against a public database of known vulnerabilities.

npm audit

A typical output looks like this:

# npm audit report

tar-fs  2.0.0 - 2.1.2
Severity: high
tar-fs can extract outside the specified directory - GHSA-pq67-2wwv-3xjx
fix available via `npm audit fix`
node_modules/tar-fs
  bundled-dependency  *
  Depends on vulnerable versions of tar-fs
  node_modules/bundled-dependency

2 vulnerabilities (1 moderate, 1 high)

You have to read it in parts: tar-fs 2.0.0 - 2.1.2 is the affected package and the vulnerable range; Severity is the assigned seriousness; the description with the GHSA-... identifier says what can be done with the flaw (this piece of information matters more than the severity); and the indented block shows the dependency chain, where bundled-dependency is what drags in the vulnerable package and therefore where you have to act.

Severities are not an automatic urgency scale:

Severity Rough meaning Reasonable reaction
Critical Remote code execution, credential theft Immediate, even on a Friday
High Privilege escalation, out-of-path writes, authentication bypass Within days
Moderate Denial of service, limited leak In the next iteration
Low Marginal or heavily conditioned impact When maintenance comes around

The important thing is that severity is computed in the abstract, without knowing how you use the package. A "high" regular-expression denial-of-service vulnerability in a library that only processes strings written by you, at build time, is not an emergency. And conversely: a "moderate" one in the path of a public HTTP request can be urgent. Before reacting, always ask yourself: does the data reaching that function come from a user?

On false positives in development dependencies: it is common for npm audit to flag dozens of problems in the build or test toolchain. They deserve attention, but of a different kind: a denial-of-service flaw in a formatter that only ever runs on your laptop is not a risk to your users. To separate signal from noise, npm audit --omit=dev shows only what reaches production and npm audit --audit-level=high only what is above a certain severity. The latter is the one worth putting in continuous integration: failing the build over any "low" in a development tool creates alert fatigue, and alert fatigue is how the critical ones end up ignored. To process the output with tooling, npm audit --json returns it structured.

  1. npm audit fix and the danger of --force

npm audit fix tries to resolve vulnerabilities while respecting the ranges declared in your package.json. If dotenv is there as ^17.2.1 and the fix is in 17.2.4, it applies it without asking: it is a compatible change according to SemVer. It is a safe operation and it is worth running regularly.

The problem is the flag the output suggests when it cannot fix something. npm audit fix --force abandons the declared ranges and installs the version that resolves the problem, even if it is a major jump: it can swap express@4 for express@5 or push a build tool up two major versions, with all the breaking changes that implies. The output warns you between the lines:

npm WARN audit Updating express to 5.1.0, which is a SemVer major change.

The rule is simple: npm audit fix yes, any time; npm audit fix --force never blindly, and never on the main branch without reviewing the git diff of package.json and the lock, and without passing the tests. If you run it by mistake:

git checkout package.json package-lock.json
npm ci

And there is a third case, the most frustrating one: npm audit reports a vulnerability with no fix available, because the intermediate package has not published a version that uses the patched dependency. That is where the two tools in the next section come in.

  1. Finding the culprit: npm ls and overrides

Before fixing anything you have to know who brings in the vulnerable package. npm ls answers that:

npm ls tar-fs
[email protected] /home/user/escena-viva
└─┬ [email protected]
  └─┬ [email protected]
    └── [email protected]

The reading is straightforward: we do not depend on tar-fs, we depend on build-tool, which uses bundler, which uses tar-fs. The options, in order of preference: update the direct dependency if a version already exists that drags in the patched one (the clean way); force the transitive version with overrides if that is not available; or replace the direct dependency if the maintainer is unresponsive.

The overrides field in package.json lets npm rewrite the resolved tree:

{
  "overrides": {
    "tar-fs": "2.1.3"
  }
}

That forces every appearance of tar-fs in the tree, wherever it comes from, to resolve to 2.1.3. If you want to be more surgical and affect only one branch:

{
  "overrides": {
    "build-tool": {
      "bundler": {
        "tar-fs": "2.1.3"
      }
    }
  }
}

After editing the manifest you have to regenerate the tree and check the result:

npm install
npm ls tar-fs
npm audit

Two warnings about overrides. First: you are installing a combination of versions that bundler's maintainer has never tested, so you need tests of your own to back the change (module 9). Second: an overrides entry is technical debt with a date on it. Put a note in the CHANGELOG or the README explaining why it is there and when it can be removed, or in a year's time nobody will dare touch it.

  1. Attacks you need to recognize

Typosquatting. Someone registers a package with a name almost identical to a popular one —expres instead of express, lodahs instead of lodash, crossenv instead of cross-env— and waits for someone to make a typo or to copy from a badly written tutorial. The fake package usually re-exports the real one —so that everything "works"— and adds a postinstall that sends your environment variables to an external server. Defense: copy names from the official documentation, not from a blog, and be suspicious of a package with 200 downloads when the one you want has millions.

Hijacking a maintainer's account. The package is legitimate and has been trustworthy for years; what changes is who controls the publishing account. It has happened through reused passwords, through phishing aimed at maintainers, and through ownership transfers to strangers who kindly offered to "help with maintenance". The malicious version gets published and spreads within hours through ^ ranges. Defense: npm ci with a pinned lock, do not upgrade blindly on release day, and review the diff when a small, stable package suddenly ships a version.

Abandoned packages. There is no attack, there is absence: nobody patches, nobody answers the issues, and vulnerabilities pile up. Warning signs: last publish three years ago, open issues with no reply, a single maintainer. It is also fertile ground for hijacking by transfer. Defense: the checklist from 05-02, applied in periodic reviews too.

Dependency confusion. A company uses an internal package called, say, escena-viva-payments, hosted in its private registry. An attacker publishes a package with that same name in the public registry and with a higher version. Depending on how resolution is configured, the client may prefer the public version. Defense: always publish under a scope of your own (@escena-viva/payments) and pin that scope to the private registry:

# Project .npmrc
@escena-viva:registry=https://internal.registry.escena-viva.test/

That way, anything under @escena-viva/ is looked up only in the internal registry, and a public namesake is irrelevant.

Protestware. The maintainer deliberately introduces non-functional behavior —political messages on the console, delays, in extreme cases deleting files based on geolocation— as a form of protest. It is not an external attack, but it breaks trust just the same and it has happened in packages with millions of weekly downloads. Defense: the same as for the rest —pinned lock, reviewed upgrades— plus an honest reflection on how many trivial dependencies your project has.

  1. Practical defensive measures

npm ci in production and in CI. We saw it back in 05-03: it installs exactly what package-lock.json says, without resolving ranges. It is what stops a version published ten minutes ago from entering your deployment.

npm ci --omit=dev

--ignore-scripts. npm ci --ignore-scripts prevents the dependencies' install hooks from running, which is where a good share of malicious code gets in. What it breaks: packages that compile native binaries or download artifacts in their postinstall become unusable —libraries with native extensions, tools that fetch a browser. Escena Viva, with dotenv and prettier, has none of those, so it can enable it without any trouble and pin it in the project's .npmrc:

engine-strict=true
ignore-scripts=true

If one particular package needs it, you run its script explicitly and consciously instead of opening the door to the hundreds of others.

Pinning critical versions. For anything touching authentication, cryptography or payments, the convenience of ^ is not worth it. Pin the exact version and upgrade by hand while reading the changelog:

"dependencies": {
  "@escena-viva/format": "0.2.1"
}

Reviewing package-lock.json in code reviews. You do not have to read the 4,000 lines; you have to look at three things: new packages nobody mentioned in the change description, changes to the resolved field pointing at a domain that is not the expected registry, and major version jumps. A lock that changes in a pull request that "only touches CSS" is a signal.

Read-only tokens. If your CI only installs, its token does not need publish permission. The token with write permission lives only in the flow that publishes, with scope limited to the specific packages (we saw it in 05-05, and we will configure it in module 11).

npm audit signatures. It verifies that the installed packages are signed by the registry and that the signatures match what was downloaded; it detects tampering in transit or on a compromised mirror. It answers with something like 214 packages have verified registry signatures in a few seconds, and it is a good candidate for the CI flow alongside npm audit --audit-level=high.

  1. Continuous maintenance: npm outdated and automation

Dependency security is not a task, it is a routine. The base tool:

npm outdated
Package    Current  Wanted  Latest  Location              Depended by
dotenv      17.2.1  17.2.4  17.4.0  node_modules/dotenv   escena-viva
prettier     3.6.2   3.6.4   4.0.1  node_modules/prettier escena-viva

Current is what is installed right now, Wanted is the highest your range in package.json allows and Latest is the last one published with the latest tag. The gap between Current and Wanted is resolved with npm update and is low risk; the gap between Wanted and Latest is a major jump and requires work: read the changelog, migrate, test.

A reasonable and sustainable update policy:

Type of change Timeframe Procedure
Security patch (high or critical) Same day npm audit fix, tests, deploy
Normal patch Weekly npm update, automated tests, merge
Minor Every two weeks or monthly Review changelog, tests, merge
Major Planned Its own branch, read the migration guide, manual testing

All of this rests on a condition we do not meet today: reliable automated tests. Without them, upgrading is a gamble and the team ends up freezing versions out of fear, which is the worst possible situation. That is why Escena Viva's test still sits at exit 1 with the message "No tests yet — Module 9": it is declared debt, not forgotten debt, and it is what will make this calendar workable.

To avoid doing it by hand there are update bots, Dependabot (built into GitHub) and Renovate. They open automatic pull requests when new versions appear, with the changelog in the description, and they let your CI decide whether they pass. A sensible configuration:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    open-pull-requests-limit: 5
    groups:
      patches:
        update-types: [patch]

Grouping patches into a single weekly pull request and leaving majors individual avoids the opposite of the intended effect: twenty open pull requests nobody reviews are worse than none.

  1. Licenses: the risk that is not technical

Every dependency brings a license, and that license imposes conditions on your product. In a personal project it gets ignored; in a company project it is a legal matter with real consequences.

License Type Practical implication
MIT, ISC, BSD, Apache-2.0 Permissive Free use, including commercial and closed source, keeping the copyright notice
LGPL Weak copyleft Usable by linking, with conditions if you modify the library
GPL, AGPL Strong copyleft It can force you to publish your product's code; AGPL also reaches software served over a network
No license All rights reserved Legally you have no permission to use it, even though it is on npm
Commercial / dual Varies It may require a purchase above a certain company size

The case that causes the most trouble in web applications is AGPL: since Escena Viva serves its functionality over the network, an AGPL dependency could force you to publish the project's entire code. And "no license" is worse than a restrictive license, because there is nothing to negotiate.

Reviewing the tree's licenses is simple: npm view dotenv license shows the one a package declares, and npx license-checker --summary walks the whole tree. In a corporate environment the norm is to have a list of approved licenses and to check it in CI. The conversation with the legal department is infinitely easier before you build the product than after.

  1. Auditing Escena Viva and writing the routine

Let's apply everything above to our project. The real dependencies are @escena-viva/format and dotenv in production, prettier and eslint in development.

npm ls --all            # 1. State of the full tree
npm audit               # 2. General audit...
npm audit --omit=dev    #    ...and only what reaches production
npm audit signatures    # 3. Registry signature verification
npm outdated            # 4. What is out of date
npm view dotenv license # 5. Licenses of the direct dependencies

The expected result is reassuring precisely because throughout modules 1 to 4 we built the entire server with the standard library. That was the best security control in the whole course: code you do not install cannot compromise you.

With the diagnosis done, we write the routine down where it can be found. We add this section to the project's README.md:

## Dependency maintenance

**Weekly** (owner: whoever is on duty)
- `npm outdated` and `npm audit`
- `npm audit fix` for anything resolvable within the ranges
- Review and merge Dependabot's grouped patch PR

**Monthly**
- Review pending minor versions by reading their changelog
- Check whether any `overrides` entry is still necessary
- `npm ls --all` and check whether an unjustified dependency has appeared

**On a high or critical severity alert**
- `npm ls <package>` to identify who drags it in
- Update the direct dependency; if there is no version, use `overrides`
- Never `npm audit fix --force` without reviewing the diff and passing the tests

**Fixed rules**
- In production and in CI: `npm ci --omit=dev`, never `npm install`
- `ignore-scripts=true` in the project's `.npmrc`
- `package-lock.json` is reviewed in every pull request
- Before accepting a new dependency: run the checklist (maintenance,
  downloads, its own dependencies, license, standard-library alternative)

And we add a script that groups the checks together, following the 05-04 principle that everything runs through the same interface:

"scripts": {
  "audit": "npm audit --audit-level=high && npm audit signatures",
  "check": "npm run lint && npm run format && npm run audit"
}

Now npm run audit is part of the project's vocabulary, and npm run check —the command the team was already using— picks up security without anyone having to remember a new command.

Common Mistakes and Tips

Running npm audit fix --force because the output itself suggests it. That text does not assess your project: it changes major versions without asking. Treat it as a migration, with a branch, a review and tests.

Treating every severity the same. A critical one in the path of a public request and a moderate one in a development formatter do not deserve the same reaction. Read the description, not just the label.

Ignoring npm audit because "it always says the same thing". Alert fatigue is how the important ones slip through. Filter with --audit-level=high and --omit=dev until the output is actionable again.

Believing the lock is a security defense. It guarantees reproducibility: you will always install the same thing, vulnerable or not. The defense is auditing, not pinning. And in deployment always npm ci, never npm install, which resolves ranges and can bring you a version published minutes ago.

Copying package names from blogs or from a generated answer. That is typosquatting's natural route. Verify the name in the official documentation and look at the downloads before installing.

Publishing internal packages without a scope. It is an open door to dependency confusion. Use @company/name and pin the scope to your registry in .npmrc.

Leaving overrides undocumented. In six months nobody will know whether it is still needed and nobody will dare remove it. Note the reason and the exit condition.

Dismissing licenses as "a lawyer thing". An AGPL in a web application's tree can have expensive consequences. It costs less to check it today.

Exercises

Exercise 1: interpret an audit

npm audit in Escena Viva reports this:

minimatch  <3.0.5
Severity: high
Regular Expression Denial of Service - GHSA-f8q6-p94x-37v3
No fix available
node_modules/minimatch
  eslint  8.0.0 - 8.57.0
  Depends on vulnerable versions of minimatch

Answer: is it an emergency for the platform's users? Which command do you use to confirm who drags in minimatch? List the resolution options, ordered from best to worst, knowing that the output says "No fix available".

Exercise 2: force a patched transitive dependency

A direct dependency of yours, [email protected], drags in [email protected], which has a critical vulnerability already fixed in [email protected]. report-generator's maintainer has not published anything in six months. Write the minimal overrides that resolves the case affecting only that branch, the commands to apply and verify it, and what you would note in the README.

Exercise 3: a suspicious pull request

A pull request arrives titled "Improve the occupancy report formatting". The diff touches src/reports/occupancy.js (12 lines), but it also adds a console-colors dependency to package.json and 340 lines to package-lock.json. List the specific checks you would run before approving it and which concrete signals would make you reject it.

Solutions

Solution 1. It is not an emergency for the users. eslint is a development dependency: it is not installed in production (npm ci --omit=dev) and it does not process data sent by a ticket buyer. Regular-expression denial of service requires a malicious input string, and here the inputs are file paths from the repository itself. It is a real finding but a low-risk one in this context; npm audit --omit=dev should stop showing it.

To confirm the chain:

npm ls minimatch

Options, from best to worst:

  1. Update eslint to a major version that already uses a patched minimatch. That is the clean solution and it affects only the tooling.
  2. If that version does not exist yet, add an overrides entry for minimatch and document that it is temporary.
  3. Accept the risk explicitly and in writing, with a review in the monthly routine, given that it is a development dependency.
  4. Replace eslint. Disproportionate for the real risk.

What you do not do is npm audit fix --force, which would probably bump ESLint by a major version and break the 05-04 configuration without warning.

Solution 2. The overrides scoped to that branch:

{
  "overrides": {
    "report-generator": {
      "csv-parser": "1.0.5"
    }
  }
}

Applying and verifying it:

npm install
npm ls csv-parser   # must show 1.0.5
npm audit           # the critical one must disappear
npm run report      # a real functional check

In the README, inside the maintenance section:

### Active overrides
- `[email protected]` under `report-generator`: patch for GHSA-xxxx.
  Remove once `report-generator` publishes a version that already includes it.
  Reviewed: 2026-08-14.

The dated note is what turns a temporary fix into something reviewable instead of a permanent mystery.

Solution 3. Checks before approving:

  1. Is the dependency justified? Twelve lines of report formatting rarely need an external package; ANSI color sequences fit in five lines of your own, and Node already offers console styling utilities.
  2. Verify the exact name against the official documentation of the package that was meant to be used: this is exactly the typosquatting pattern.
  3. npm view console-colors: date of the last publish, download count, maintainers, license and —very important— its own dependencies.
  4. Look for postinstall in the package's package.json and in the lock.
  5. Review the resolved fields in package-lock.json: they must point at the expected registry and not at an arbitrary domain or repository.
  6. Check how many new packages appear: 340 lines of lock for a color utility suggests a disproportionate transitive tree.
  7. Run npm audit and npm audit signatures on the branch.

Signals for outright rejection: the name does not match any known package and has few downloads; there is an install script; resolved points outside the registry; the package was published days ago; or the functionality simply does not need a dependency. In this case, the reasonable thing is to ask for the color to be implemented in the project itself and for the pull request to stay at the 12 lines its title announced.

Conclusion

Installing means running someone else's code with your permissions. That sentence sums up the lesson and justifies everything else: npm audit to know what is there, reading the flaw's description and not just the severity label; npm audit fix as a routine and --force never blindly; npm ls <package> to locate who drags in the vulnerable one and overrides —documented and dated— to force a patched transitive dependency when there is no clean way out. We have learned to recognize typosquatting, hijacked accounts, abandoned packages, dependency confusion and protestware, and to defend ourselves with concrete measures: npm ci in production, ignore-scripts in .npmrc, pinned versions for anything critical, lock review in every pull request, read-only tokens and npm audit signatures. And we have written a maintenance routine into the README —patches fast, majors calmly— that will only be sustainable once we have the module 9 tests, plus a look at licenses, which are a business risk even when they are not a technical one.

That closes module 5 completely. We started with the manifest: npm init, package.json field by field, node_modules, npx and .npmrc. We moved on to installing: dependencies versus devDependencies, the flattened tree and its phantom dependencies, and the checklist for accepting a dependency before installing it. Then came versioning: SemVer, the ranges and their ^0.x.y trap, package-lock.json with its integrity and its resolved, and npm ci as the correct way to install on machines that are not yours. Then the scripts, turned into the project's single interface, with the node_modules/.bin PATH, the hooks and their risks. In the fifth lesson we switched sides of the counter and published @escena-viva/format, deciding with exports what is public API and with files what travels to the registry. And here we have learned to live with all that third-party code without being naive. Escena Viva is no longer just an application that works: it is a project with a manifest, reproducible versions, standardized commands, a published package of its own and a written security policy.

In module 6 Express arrives, and it arrives at the best possible moment. We are going to replace the handmade router we wrote in module 4 with a framework, and the experience will be different from the usual one: where others see magic, you are going to recognize parts. Express's app.get('/api/events/:id') is the router.js you built comparing paths and extracting parameters by hand. Its middleware is the chain of functions you already chained around body.js. Its res.json() is your responses.js. Its error handler is your http-errors.js. And you will look at every dependency Express brings with it through the eyes you have just trained in this lesson. We will start with an introduction to Express: what problem it solves exactly, what it saves you and what remains your responsibility.

Node.js Course: From Beginner to Advanced

Module 1: Introduction to Node.js

Module 2: Core Concepts

Module 3: File System and I/O

Module 4: HTTP and Web Servers

Module 5: NPM and Package Management

Module 6: The Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2026. All rights reserved