vulnerable-nodejs — a real shop (NodeBazaar) intentionally full of security bugs.
14 labs · OWASP Top 10:2025 · SAST benchmark · Node.js port of DVWA
Express · Postgres · Mongo · Docker Compose
Inspired by DVWA · Author Daniel Alfocea · BSD
no mocks · no fake vulns · real PostgreSQL on the other end
This is not a toy mock. NodeBazaar is a working storefront: catalog, product pages, reviews, guest checkout with a Stripe-style test card, orders, account, admin tools, and a small JSON API. The bugs are real code paths against real Postgres and Mongo. When you inject SQL, you hit PostgreSQL. When you SSRF, the app reaches services on the Docker network. Nothing is faked with a hardcoded "hack succeeded" string.
Local use only. Never expose this stack to the internet.
| Product | Checkout | Orders | Sign in |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
The original vulnerable-node (2016) had one goal: real vulnerable Node.js code — not simulated — to measure the quality of security analyzers and train pentesters. This rewrite keeps that mission and adds what the original never had: a complete application to protect, exploit guides, and fixes.
Most "vulnerable apps" look like demo shells. NodeBazaar looks and behaves like a shop you would ship, then leaves the doors open on purpose so you can practice finding and fixing them.
Every risky spot is marked in the source:
VULN:what is wrong and why it mattersSAFE:what to do instead
You can learn from the UI, from docs/labs/, or by reading the code next to the bug.
docker compose up --buildDefault host port is 8888 so it does not fight proxies on 8080 (Caido and friends). Override with APP_PORT=9999 if needed.
You only need Docker. No host Node.js, Postgres, or Mongo.
The catalog is public — like a real shop. Login is only required to post reviews or see order history; checkout works as guest too (and the guest flow is where CSRF and price tampering get interesting).
Optional: import postman/NodeBazaar.postman_collection.json (baseUrl = http://localhost:8888).
| Username | Password | Role |
|---|---|---|
| alice | alice123 | customer |
| bob | bob123 | customer |
| admin | admin123 | admin |
Test card: 4242 4242 4242 4242
| # | Vulnerability | OWASP Top 10:2025 | Where | Lab |
|---|---|---|---|---|
| 01 | BOLA / IDOR on orders | A01 Broken Access Control | routes/orders.js, routes/api/v1.js |
docs |
| 02 | SSRF → IMDS → vault | A01 (SSRF) | routes/tools.js, mocks/ |
docs |
| 03 | SQL injection: login, search, sort | A05 Injection | model/db.js |
docs |
| 03b | NoSQL injection on Mongo filters | A05 Injection | routes/shop.js + Mongo |
docs |
| 04 | Mass assignment → admin | A06 Insecure Design | routes/auth.js, routes/account.js |
docs |
| 05 | Stored XSS + token theft | A05 Injection | reviews + localStorage |
docs |
| 06 | Checkout price tampering | A06 Insecure Design | checkout amount_cents |
docs |
| 06b | CSRF (no tokens anywhere) | A01 | every POST route |
docs |
| 07 | Forgeable JWTs (/api/v1/*) |
A07 Authentication Failures | config.js, middleware/auth.js |
docs |
| 09 | Debug + verbose errors | A02 Security Misconfiguration / A10 | DEBUG_ERRORS, search |
docs |
| 10 | Secrets in logs + log injection | A09 Logging & Alerting Failures | login / contact prints | docs |
| 11 | Poisoned CI pipelines | A03 / A08 | .github/workflows/, azure-pipelines.yml |
docs |
| 12 | Secret still in git history | A02 / A04 | early .env commit |
docs |
| 13 | Prototype pollution via merge | A08 Software/Data Integrity Failures | model/merge.js, /preferences |
docs |
| 14 | ReDoS: event loop outage | A05 / availability | routes/tools.js regex tester |
docs |
| 15 | Path traversal on downloads | A01 (CWE-22) | routes/extras.js |
docs |
| 16 | Command injection (ping tool) | A05 (CWE-78) | routes/extras.js |
docs |
| 17 | Docker deployment hardening | A05 / A02 | Dockerfile, docker-compose.yml |
docs |
Cookie session drives the HTML shop. Broken JWTs are only used under /api/v1/*.
Full walkthroughs with short fixes: docs/labs/.
Open any route under routes/ and search for VULN: / SAFE:. The teaching note sits next to the line that fails review in real PRs. Example idea:
// VULN (A01 BOLA): login checked, ownership not.
// SAFE: add "AND user_id = $2" with the session user id.
const { rows } = await db.orderById(req.params.id);npm test # 17 tests — 13 exploit checks + 4 dashboard, all must PASS
npm run verify:fixes # after you fix the labs: exit 0 means all exploits fail
bash smoke.sh # container hardening smoke (non-root, read-only fs)tests/exploits.js holds one exploit definition per lab; the test suite documents the broken state and verify:fixes inverts the assertions to check your fixes. The hardened container runs as node with a read-only rootfs — see lab 17.
- Node.js 22 + Express 4 (server-rendered EJS UI + small JSON API)
- PostgreSQL via
pg - MongoDB (tag filter / NoSQL lab)
- Plain CSS + vanilla JS, no build step, zero frontend dependencies
- Docker Compose
- Local IMDS + vault mocks for SSRF
.github/workflows/ci.yml and azure-pipelines.yml are the files GitHub/Azure DevOps would pick up. They are unsafe on purpose (PR builds reach secrets, untrusted values land in shell). Compare with ci-hardened.yml and pipelines/azure-pipelines.hardened.yml. Do not point either at real credentials.
An early commit added a fake .env with PIPELINE_TOKEN. A later commit removed it from HEAD. The value remains in history:
git log -p --all -S 'VN-FAKE' -- .envThe original reason this repo exists: measuring how well source code analyzers (SAST) do against real Node.js code. sast/ keeps that mission: self-contained vulnerable seed files with a ground-truth manifest, so you can run any analyzer and score true/false positives.
See sast/README.md.
NodeBazaar — vulnerable-nodejs/README.md conflict resolution: keep both sections, they document different features
Log in and open /labs — a live dashboard that runs the real
exploits from tests/exploits.js against the app as your own user and shows which labs you have
exploited (✅/❌ per card, with a progress bar). Mutating labs (04/05/06) are restored afterwards.
The sast-benchmark workflow runs semgrep and njsscan against sast/seeds/ on every push to master, scores them with the ground-truth manifest, and publishes a TPR/FPR table to the Actions tab. Reference numbers and the local runner live in sast/README.md.
server.js Express app (VULN / SAFE commented)
routes/ Shop, pages, auth, orders, account, tools, /api/v1
model/ Data access (vulnerable queries) + merge gadget
middleware/ Session + JWT guards
views/, public/ EJS templates and assets
mocks/ Fake IMDS + vault for the SSRF lab
db/ Schema + seed
docs/labs/ Exploit guides + fixes
docs/screenshots/ UI captures for this README
postman/ Postman collection
sast/ Analyzer benchmark seeds + ground truth
.github/workflows/ GitHub Actions (unsafe + hardened)
azure-pipelines.yml, pipelines/ Azure DevOps (unsafe + hardened)
Daniel Alfocea — daniel@danielalfocea.com
Original vulnerable-node (2016) by Daniel García (cr0hn).
BSD (LICENSE). For learning and local research. Not a real store, even when it looks like one.





