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
- The supply chain as an attack surface
npm audit: really reading the outputnpm audit fixand the danger of--force- Finding the culprit:
npm lsandoverrides - Attacks you need to recognize
- Practical defensive measures
- Continuous maintenance:
npm outdatedand automation - Licenses: the risk that is not technical
- Auditing Escena Viva and writing the routine
- Common Mistakes and Tips
- Exercises
- Conclusion (closing Module 5)
- 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
.envfiles. - Open network connections and send whatever it has read to any server.
- Run arbitrary commands through the
preinstall,installandpostinstallhooks 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 |
npm audit: really reading the output
npm audit: really reading the outputnpm audit compares the resolved dependency tree (the one in package-lock.json) against a public database of known vulnerabilities.
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.
npm audit fix and the danger of --force
npm audit fix and the danger of --forcenpm 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:
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:
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.
- Finding the culprit:
npm ls and overrides
npm ls and overridesBefore fixing anything you have to know who brings in the vulnerable package. npm ls answers that:
[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:
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:
After editing the manifest you have to regenerate the tree and check the result:
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.
- 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:
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.
- 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.
--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:
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:
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.
- Continuous maintenance:
npm outdated and automation
npm outdated and automationDependency security is not a task, it is a routine. The base tool:
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.
- 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.
- 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 dependenciesThe 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:
Options, from best to worst:
- Update
eslintto a major version that already uses a patchedminimatch. That is the clean solution and it affects only the tooling. - If that version does not exist yet, add an
overridesentry forminimatchand document that it is temporary. - Accept the risk explicitly and in writing, with a review in the monthly routine, given that it is a development dependency.
- 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:
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 checkIn 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:
- 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.
- Verify the exact name against the official documentation of the package that was meant to be used: this is exactly the typosquatting pattern.
npm view console-colors: date of the last publish, download count, maintainers, license and —very important— its own dependencies.- Look for
postinstallin the package'spackage.jsonand in the lock. - Review the
resolvedfields inpackage-lock.json: they must point at the expected registry and not at an arbitrary domain or repository. - Check how many new packages appear: 340 lines of lock for a color utility suggests a disproportionate transitive tree.
- Run
npm auditandnpm audit signatureson 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
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
