Self-hosted service
snyk-labs/nodejs-goof avatar
snyk-labs/nodejs-goof

nodejs-goof is Snyk's deliberately broken todo app for testing scanners

Super vulnerable todo list application

548 stars3,643 forksJavaScriptApache-2.0

At a glance

What is it?
nodejs-goof is a vulnerable Node.js demo application that exists so security scanners can be pointed at something and produce visible findings. It combines known-vulnerable npm dependencies with code-level flaws in an Express and MongoDB app, and it pins old software on purpose, so the interesting question is what it is a test fixture for rather than whether it should ever be deployed.
Who is it for?
Adopt nodejs-goof if you are evaluating a security scanner and need a target that produces visible, reproducible findings across dependency, container and runtime analysis. Do not adopt it as application code or as a starting point for a todo list, because every package in it is pinned to a version with a known flaw and the README states the application has to run an old MongoDB.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A test fixture, not a sample application

The repository description is one word: a super vulnerable todo list application. The README calls it a vulnerable Node.js demo application, based on the Dreamers Lab tutorial on writing a todo list with Express and MongoDB.

The important context comes from the owner. This sits under snyk-labs, the research arm of Snyk, and the README carries a badge linking to Snyk's own vulnerability database entry for the repository. The application exists so that Snyk's scanners, and anyone else's, can be pointed at a codebase and produce findings you can see.

That framing changes how you read everything else. The old package versions are not neglect, they are the fixture. The insecure code is not carelessness, it is the second half of the test case, since a scanner that only finds known-vulnerable dependencies has not been tested against the flaws in your own code.

The README states the scope of the experiment in three bullets. There are exploitable packages with known vulnerabilities, there is Docker image scanning for base images with known vulnerabilities in system libraries, and there are runtime alerts for detecting an invocation of vulnerable functions in open source dependencies.

Those three map onto three different products. The first is software composition analysis over the dependency tree. The second is container image analysis, which is why the Dockerfile matters as much as package.json. The third is runtime or dynamic analysis, which is why the application is designed to actually call the vulnerable functions rather than merely import them, so that a runtime agent watching the process has something to observe.

The application itself is ordinary. Express serves routes, Mongoose talks to MongoDB, Handlebars renders views, and there is a todo list with an account details form. That ordinariness is deliberate too, because a scanner that only works on contrived code is not much use.

The four vulnerable dependencies and why the versions matter

The README lists four exploitable packages, each with a link to the corresponding Snyk vulnerability database entry.

Mongoose has a buffer memory exposure issue, and the entry notes that it requires a version at or below Node.js 8. For the exploit demo purposes the README says you can update the Dockerfile node base image to use FROM node:6-stretch. That instruction is worth reading carefully, because the Dockerfile currently ships FROM node:18.13.0 and keeps the older FROM node:6-stretch as a commented-out line above it. The default image will not trigger that particular vulnerability, and the fixture expects you to change it.

The other three are st with a directory traversal issue, ms with a regular expression denial of service, and marked with a cross-site scripting issue. The versions pinned in package.json confirm the intent: st is at 0.2.4, ms at 0.7.1, marked at 0.3.5, and mongoose at 4.2.4. Those are versions from around 2015 and 2016, and the package-lock.json is committed so that the vulnerable tree resolves the same way on every machine.

The dependency list in package.json is otherwise a snapshot of a mid-2010s Express application: express at 4.12.4, body-parser at 1.9.0, lodash at 4.17.4, ejs at 1.0.0, consolidate at 0.14.5, and both dustjs-helpers and dustjs-linkedin as templating helpers alongside hbs for Handlebars. There is also mysql alongside mongodb, and two separate database modules at the repository root, mongoose-db.js and typeorm-db.js, plus adm-zip, file-type, method-override and express-fileupload.

The scripts block is short and tells you how the project is meant to be exercised. The test script is snyk test, so running the project's own test target invokes the scanner rather than a unit test suite. The build script runs browserify over jquery into public/js/bundle.js, the cleanup script removes every todo from the database, and both dev and start set NODE_OPTIONS=--openssl-legacy-provider, which is the flag needed to run these old packages on a modern Node.

Running it locally, and the MongoDB version constraint

The README's running section is four commands, and the first one is the database:

bash
mongod &
bash
git clone https://github.com/snyk-labs/nodejs-goof
npm install
npm start

That starts Goof locally, using a local MongoDB on the default port, with the application listening on port 3001 at http://localhost:3001.

The constraint that catches people out is stated immediately after: you have to use an old version of MongoDB due to some of these old libraries' database server APIs, and MongoDB 3 is known to work. The driver and the ODM code in the repository predate the MongoDB wire protocol changes that came later, so a current server simply refuses the connection handshake. This is the clearest illustration of what a pinned-vulnerability fixture actually costs you, since the dependency constraint propagates all the way to the infrastructure.

If you would rather not install MongoDB locally, the README shows the container route for the database alone:

sh
docker run --rm -p 27017:27017 mongo:3

Or you can bring up the whole stack with the compose file:

bash
docker-compose up --build
docker-compose down

The compose file is a good thing to read, because it is where the two scanning targets meet. The goof service builds from the local Dockerfile, sets the DOCKER=1 environment variable so the application knows it is containerised, and publishes two ports: 3001 for the web application and 9229 for the Node inspector. The second one is there so a runtime agent can attach to the process and watch which functions get called, which is how the runtime alerting scenario is exercised.

The compose file also defines a goof-mongo service running the mongo:3 image on port 27017, and a third service, named good-mysql in the file, running mysql:5 with a root password and an acme database on port 3306. The service is not referenced by the others, which suggests it is there so that the MySQL code path can be exercised separately.

To clear the todo list between runs, the cleanup script drops the collection directly through the mongo shell:

bash
npm run cleanup

The repository also carries a deploy-heroku.md, and the README describes what each platform needs. Heroku requires attaching a MongoLab service, which sets the MONGOLAB_URI environment variable. CloudFoundry requires attaching a MongoLab service and naming it goof-mongo, because the code explicitly looks for the credentials of a service with that name.

The code-level flaws are the half that cannot be upgraded away

The README lists the vulnerabilities in the code separately from the dependency ones, and the list is the actual argument for having a fixture at all: open redirect, NoSQL injection, code injection, command execution, cross-site scripting, information exposure through hardcoded values, a security misconfiguration that exposes server information, and insecure HTTP communication.

The code injection explanation is the most instructive, because it shows a class of bug that no dependency upgrade can fix. The page at /account_details is rendered as a Handlebars view, and the same view handles both the GET request that shows the details and the POST that updates them. The form is described as completely functional, and it receives the profile information from req.body and passes it as-is to the template.

The README's own framing of the flaw is precise: you would think the worst that can happen is that a validation confirms the expected input, but the validation does not account for a new field that can be added to the object, such as layout, which when passed to a template language could lead to Local File Inclusion and path traversal vulnerabilities. The vulnerability is not the field that was validated, it is the field that was not, and the validation is what creates the false sense of safety.

The NoSQL injection entry explains itself in the same way. The login handler is exposed as a controller in routes/index.js, and it uses the User.find() query with email as username and password. The README notes the plaintext password storage, then says however there are other issues in play here, and asks what happens if the password is not a string but an object. The answer given is that the object structure is passed as-is to the password property, and MongoDB gives it a specific meaning through the $gt operation, so the request matches the admin user against any record whose password is greater than the empty string.

There is even a second bug in the same code. The validator library used by the application has several known regular expression denial of service vulnerabilities, one associated with the email regex, which if validated with the allow_display_name option allows the route to be denied service. The validator.rtrim() sanitizer is described as similarly vulnerable.

The exploits/ directory in the repository contains a series of steps demonstrating each one, and there is an example111.json at the root and a sarif.json for machine readable scanner output.

The Dockerfile and the two scanning surfaces

The Dockerfile is short and it exists for two reasons. The first is to run the application, and the second is to be something to scan.

The base image is FROM node:18.13.0, with the FROM node:6-stretch line kept above it as a comment, which is the older image the Mongoose buffer exposure demo needs. The build creates /usr/src/goof and /tmp/extracted_files, copies the source in, sets the working directory, runs npm update and then npm install, and exposes both 3001 and 9229 before setting the entrypoint to npm start.

The RUN npm update step is the interesting one for anyone reading the file as a scanner target. The committed package-lock.json pins the vulnerable versions, and npm update without a version argument refreshes those to the latest allowed by the version ranges in package.json. Whether that closes the Mongoose issue or not depends on whether the range still permits the vulnerable version, which is exactly the kind of question a scanner is supposed to answer and a human reading a lockfile often gets wrong.

The node:18.13.0 base is itself a scanning target, because the base image brings its own system libraries with their own vulnerabilities. The README's second feature bullet is Docker image scanning for base images with known vulnerabilities in system libraries, and the fact that the Dockerfile is checked in, next to the compose file, is what makes that test reproducible.

For the runtime scenario, the inspector port is the mechanism. Publishing 9229 lets an agent attach to the Node process and observe which vulnerable functions are invoked while the application runs, which is a different question from the static question of what is present in the tree. A dependency can be present and never called; runtime alerting is about the difference.

The repository structure supports all three scenarios. There are routes/ and views/ for the application code, service/ and entity/ directories, public/ for the browserified bundle, utils.js at the root, a Dockerfile and docker-compose.yml at the top level, and a tests/ directory alongside the exploits/ directory that holds the demonstration steps.

Where this repository stops being useful

The first limit is the obvious one. It is not an application to deploy, not an example to copy from, and not a starting point for a todo list. The package.json dependencies are pinned to versions with published vulnerabilities, the code contains deliberate flaws in the authentication and template handling, the README lists hardcoded credential exposure as a feature, and the application communicates over HTTP.

The second limit is that the fixture ages. The vulnerable dependency versions were current in 2015 and 2016, and the README's own instruction to use MongoDB 3 is a symptom of the same drift. A scanner's results against this tree will reflect what those old packages contained, which is still a valid test, but it is not a test of how the scanner handles a current ecosystem.

The third limit is that it tests finding, not fixing. The repository will produce findings; it will not tell you how to remediate a specific one in your own codebase, and the exploits/ directory demonstrates the vulnerability rather than the repair.

The alternative to weigh is a different kind of fixture. A scanner vendor's own test suite is the closest comparison, and there are also long-standing deliberately vulnerable applications such as OWASP Benchmark, which focuses on static analysis accuracy against hand-written flaws rather than on the combination of vulnerable dependencies, vulnerable base images and runtime behaviour that Goof covers. The difference in approach is scope. A benchmark suite answers the question of whether a tool reports a known line of vulnerable code correctly. Goof answers the question of whether the whole pipeline, from dependency analysis through image scanning to runtime alerting, produces visible output on an application that a developer would recognise as ordinary. If you are choosing a target for a scanner evaluation, that distinction matters more than the individual flaws.

On maintenance and terms, the last push was on 2026-10-09 and the repository is not archived, so the fixture is still being kept current in some sense. The repository has no GitHub releases, so there is no tagged version to pin. The licence is Apache-2.0, recorded in package.json and with a LICENSE file at the root, which is a permissive grant with no copyleft obligation. That is a licensing note rather than legal advice, and it matters here in a practical way: because the code is permissively licensed, anyone can fork it and adjust the vulnerable versions, which is exactly what the README's suggestion of editing the Dockerfile base image invites you to do.

Editorial conclusion

Adopt nodejs-goof if you are evaluating a security scanner and need a target that produces visible, reproducible findings across dependency, container and runtime analysis. Do not adopt it as application code or as a starting point for a todo list, because every package in it is pinned to a version with a known flaw and the README states the application has to run an old MongoDB. Verify first that you run it only on a local machine or an isolated network, that the MongoDB 3 requirement is satisfied, and that the port 3001 and port 9229 it exposes are not reachable from anywhere you did not intend.

Frequently asked questions

What is snyk-labs/nodejs-goof used for?

It is a deliberately vulnerable Node.js demo application used to demonstrate security scanning. The README lists three capabilities to experiment with: exploitable packages with known vulnerabilities, Docker image scanning for base images with known vulnerabilities in system libraries, and runtime alerts for detecting an invocation of vulnerable functions in open source dependencies.

How do I run nodejs-goof locally?

Start MongoDB, then clone the repository with git clone https://github.com/snyk-labs/nodejs-goof, run npm install and npm start, and open http://localhost:3001. You have to use an old MongoDB version because of the old libraries' database server APIs, and MongoDB 3 is known to work. The alternative is docker-compose up --build.

Which npm packages in nodejs-goof have known vulnerabilities?

Four are listed: Mongoose with a buffer memory exposure issue, st with a directory traversal issue, ms with a regular expression denial of service, and marked with a cross-site scripting issue. The versions are pinned in package.json and package-lock.json, and the Mongoose demo needs a Node.js 8 or older base image, so the Dockerfile currently comments out FROM node:6-stretch in favour of FROM node:18.13.0.

What code-level vulnerabilities does nodejs-goof demonstrate?

The README lists open redirect, NoSQL injection, code injection, command execution, cross-site scripting, information exposure via hardcoded values, a security misconfiguration exposing server information, and insecure HTTP communication. The exploits/ directory contains a series of steps demonstrating each one, and the code injection example runs through the /account_details Handlebars view, where validation does not cover an added field such as layout.

What licence is nodejs-goof released under?

Apache-2.0, recorded in the license field of package.json and with a LICENSE file at the repository root. The last push was on 2026-10-09, the repository is not archived, and it publishes no GitHub releases.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. snyk-labs/nodejs-goof on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/snyk-labs-nodejs-goof.svg)](https://hysenlabs.com/projects/snyk-labs-nodejs-goof)

Was this useful?