October 10, 2026
Pwnshop: a vulnerable web app for web and AI security training
Over 50 intentional vulnerabilities across the OWASP Top 10 and the OWASP LLM Top 10. Hereβs what Pwnshop is and how to get it running withβ¦

By Ibrahim Mutiu Olajide
9 min read
Over 50 intentional vulnerabilities across the OWASP Top 10 and the OWASP LLM Top 10. Here's what Pwnshop is and how to get it running with Docker.
Most practice apps teach a handful of bugs on pages that look nothing like a real product. I built Pwnshop to close that gap. It's an online shop with sellers, a wallet, one-time-code logins, an admin panel, and an AI shopping assistant, and almost every part of it is broken on purpose.
This post explains what Pwnshop is and walks you through running it on your own machine with Docker. By the end, you'll have the shop running at http://localhost:3000 and be logged in as a test user. You need basic terminal skills. I explain every command along the way.
Disclosure:_ I'm the author of Pwnshop. I built it for the CTF Security training programme._
Warning:_ Pwnshop is deliberately vulnerable. Run it only on your own machine or an isolated lab network, and never expose it to the internet._
Quick start (if you already know Docker):
git clone https://github.com/ctfsec/pwnshop.git
cd pwnshop
chmod +x setup-local.sh
./setup-local.shgit clone https://github.com/ctfsec/pwnshop.git
cd pwnshop
chmod +x setup-local.sh
./setup-local.shThen open http://localhost:3000. This works as written on Linux. If you're on macOS or Windows, or you want the chatbot, read the full steps below.
What Pwnshop is
Pwnshop is a deliberately vulnerable e-commerce web app for practising web and AI security testing. You browse products, buy from sellers, top up a wallet in Naira, and chat with an AI assistant, just like on a real shop. The difference is that the shop is full of documented flaws you're allowed to attack.
Most practice labs feel like they were built for somewhere else. I wanted one that looks like the shops my students actually use: prices in Naira, wallet top-ups, and login codes that land in an inbox. When the setting feels familiar, it's much easier to picture the same bug in an app you use every day. Pwnshop started as a training exercise for the CTF Security programme and grew into a full shop with dozens of holes in it, every one of them on purpose.
The app runs on Node.js 18 with Express, EJS templates, and a MySQL 5.7 database. The chatbot calls a large language model through the Groq API. Logins use a one-time code that arrives in PwnMail, an inbox built into the app, so you don't need a real email account.
Here's how the vulnerabilities break down:
- 35 web app flaws and business logic bugs, mapped to the OWASP Top 10 (2025). These include SQL injection, stored XSS, SSRF, template injection that leads to remote code execution, and prototype pollution.
- 19 AI assistant flaws, mapped to the OWASP Top 10 for LLM Applications (2026). These include prompt injection, agent command injection, and data leaks through a shared answer cache.
- 4 payment flow flaws, such as overriding the credited amount and skipping payment verification.
The payment flaws need extra credentials for an external payment service, so most readers will practise the other 54. The full list, with hints, lives inside the app at /vulnerabilities.
That list covers the flaws I planted deliberately, but it may not be the full story. Pwnshop is a real codebase, and real codebases have bugs their authors never intended. If you find one that isn't on the list, I'd love to hear about it, so please report it on the Pwnshop GitHub repository.
What I like most is that the flaws chain together. For example, a debug page leaks the session secret, and that secret lets you forge an admin cookie without ever logging in. Chains like this are how real breaches happen, and they're hard to practise on labs that isolate one bug per page.
What our customers are saying
Every shop needs testimonials, so here are a few from Pwnshop's most loyal users. None of them are real, which makes them the most honest thing in the app.
"Bought a laptop for β¦0.01. Delivery was fast. Payment verification was faster, because it never happened."
"I came for a phone case and left as the admin. Would escalate again."
"The AI assistant gave me a free order. When I asked why, it said I told it to. I did."
"I reported a bug to support. Support replied: which one?"
If you winced at any of these, good. That's what a training lab is for: you laugh at the bug here, so you don't meet it at 2 a.m. on a production server.
What you need
- Docker Engine 20 or later with Docker Compose v2. On Linux, install Docker Engine from your distribution's instructions on docs.docker.com. On macOS or Windows, install Docker Desktop.
- Git, to download the code.
- About 600 MB of data for the first setup, and about 2 GB of free disk space.
- A free Groq API key (optional). You only need it for the AI chatbot. Get one at console.groq.com.
If you're on mobile data, plan the download. Here's where the 600 MB goes:
- Node.js 18 base image (
node:18-bullseye): 368 MB - MySQL 5.7 image (
mysql:5.7): 138 MB - The Pwnshop repository (
git clone): 18 MB - npm packages for the app: 11 MB
- Debian packages (MySQL client and
gosu): 10 MB
That's about 545 MB in total.
Image sizes are the compressed amd64 images listed on Docker Hub, as of October 2026. Running the setup a second time costs almost no data, because Docker keeps the images it has already downloaded.
Note for Apple Silicon Macs: the mysql:5.7 image only exists for Intel/AMD (amd64) processors. Docker Desktop runs it through emulation, which is slower. I tested this guide on Linux.
Set up Pwnshop with Docker Compose
Docker Compose starts two containers: one for the Pwnshop app and one for its MySQL database. A setup script does most of the work, so there are only five steps.
Step 1: check that Docker works
Run these two commands. Each one prints a version number.
docker --version
docker compose versiondocker --version
docker compose versionYou need Docker 20 or later and Compose v2 or later. If docker compose returns an error, you probably have the older docker-compose tool, and you need to install the Compose v2 plugin.
Step 2: download the code
Clone the repository and move into its folder:
git clone https://github.com/ctfsec/pwnshop.git
cd pwnshopgit clone https://github.com/ctfsec/pwnshop.git
cd pwnshopThis downloads about 18 MB. If GitHub is slow for you, the same code is mirrored at github.com/r007sec/pwnshop.
Step 3: create the lab settings file
The setup script reads its settings from a file called .env.lab. Create it with random values for the database password and the reset token:
cat > .env.lab <<EOF
DB_NAME=pwnshop
DB_PASSWORD=$(openssl rand -hex 16)
LAB_RESET_TOKEN=$(openssl rand -hex 16)
HEAL_EVERY_MINUTES=20
GROQ_API_KEY=
EOFcat > .env.lab <<EOF
DB_NAME=pwnshop
DB_PASSWORD=$(openssl rand -hex 16)
LAB_RESET_TOKEN=$(openssl rand -hex 16)
HEAL_EVERY_MINUTES=20
GROQ_API_KEY=
EOFHere's what each line does:
DB_PASSWORDis the MySQL root password.openssl rand -hex 16generates a random 32-character value.LAB_RESET_TOKENprotects the reset endpoint used by instructor tooling. The browser reset page doesn't need it, but keep the file in case you automate resets later.HEAL_EVERY_MINUTESsets how long the lab waits with no activity before it resets itself. Set it to0to turn this off.GROQ_API_KEYis for the chatbot. Paste your key after the =, or leave it empty to skip the chatbot.
If you skip this step, the script generates .env.lab for you automatically, with a random database password and reset token, and prints them when it finishes.
Step 4: run the setup script
Make the script executable, then run it:
chmod +x setup-local.sh
./setup-local.shchmod +x setup-local.sh
./setup-local.shThe script works through five stages, and it numbers them in the output:
- Removes any containers and data left over from an earlier setup.
- Builds the app image and starts both containers. This is the slow part on a first run, because Docker downloads the images listed earlier.
- Waits until MySQL accepts connections.
- Creates the database and imports the shop's products, users, and orders from
pwnshop.sql. - Copies the product images into the app's upload folder. This step asks for your
sudopassword once.
The output looks like this:
[1/5] Cleaning up existing containers and volumes...
[2/5] Building and starting containers...
[3/5] Waiting for MySQL to be ready...
... ready!
[4/5] Creating database and importing schema...
Database imported successfully (21 tables).
[5/5] Copying seed images...
[sudo] password for r007:
Seed images copied.
ββββββββββββββββββββββββββββββββββββββββββββ
β PwnShop is ready! β
β Visit: http://localhost:3000 β
ββββββββββββββββββββββββββββββββββββββββββββ
DATABASE CREDENTIALS (Internal Container):
Username : root
Password : β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’
Database : pwnshop
------------------------------------------------
LAB RESET TOKEN: β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’[1/5] Cleaning up existing containers and volumes...
[2/5] Building and starting containers...
[3/5] Waiting for MySQL to be ready...
... ready!
[4/5] Creating database and importing schema...
Database imported successfully (21 tables).
[5/5] Copying seed images...
[sudo] password for r007:
Seed images copied.
ββββββββββββββββββββββββββββββββββββββββββββ
β PwnShop is ready! β
β Visit: http://localhost:3000 β
ββββββββββββββββββββββββββββββββββββββββββββ
DATABASE CREDENTIALS (Internal Container):
Username : root
Password : β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’
Database : pwnshop
------------------------------------------------
LAB RESET TOKEN: β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’Your database password and reset token will be different. I've hidden mine. If you skipped Step 3, you'll also see a line saying the script generated
.env.labfor you.
macOS and Windows: stage 5 copies images into a folder that only exists on Linux hosts, so on Docker Desktop the script stops at this step. The app and database are already running by then. Open http://localhost:3000 and, if product pictures are missing, run the terminal reset command from the "Reset, stop, and restart the lab" section to restore them.
Step 5: check that the shop is running
Open http://localhost:3000 in your browser. You see the Pwnshop homepage with product listings and featured sellers.
The app only listens on 127.0.0.1, which means other devices on your network can't reach it. That's what you want for a vulnerable app.
To confirm the app started fully, open http://localhost:3000/debug/info. It returns some JSON. (This page is itself one of the vulnerabilities, so don't be surprised by what's in it.) If it returns a 404 page instead, see the troubleshooting section below.
Log in as a test user
The database comes with four ready-made accounts. You can log in with the username or the email address.
Role Username Password
Seller alice alice123
Seller pwnshop pwnshop123
Seller Mahveen mahveen123
Buyer olajide olajide123Role Username Password
Seller alice alice123
Seller pwnshop pwnshop123
Seller Mahveen mahveen123
Buyer olajide olajide123There's also an admin account, but its password isn't published. Getting into the admin panel is one of the challenges.
Logging in takes two steps, like on many real shops:
- Go to
http://localhost:3000/loginand enter a username and password. - The app sends a six-digit code to the user's in-app inbox and shows a code entry page. Click Open Pwnshop Inbox on that page to open the inbox in a new tab, copy the code, and paste it back.
Codes expire after 10 minutes. If yours expires, log in again to get a new one.
Once you're in, open http://localhost:3000/vulnerabilities. This page lists every vulnerability with a description and a hint, so it's a good place to pick your first target.
Reset, stop, and restart the lab
Attacking the shop changes its data. To restore it, open http://localhost:3000/reset, tick the confirmation box, and click Reset the Lab. No token required. This wipes all student-created data (accounts, orders, cart items, wishlists, reviews, OTPs, PwnMail, student-added products and coupons) while preserving the seeded demo accounts, products, coupons and wallet balances, as the page's "Deleted / Preserved" columns show.
There's also a separate programmatic endpoint,
POST /lab/reset, used for automation/instructor tooling, which does require theLAB_RESET_TOKENfrom your.env.lab. The browser button does not use it.
The Compose file lives in the docker folder, so every Compose command needs to point at it. Run these from the pwnshop folder:
# Stop the lab and keep its data
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml down
# Start it again
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml up -d
# Reset from the terminal instead of the browser
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml exec app bash /usr/src/app/scripts/reset-lab-inside.sh# Stop the lab and keep its data
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml down
# Start it again
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml up -d
# Reset from the terminal instead of the browser
docker compose --env-file .env.lab -f docker/docker-compose.lab.yml exec app bash /usr/src/app/scripts/reset-lab-inside.shIf the lab gets into a state you can't recover from, run ./setup-local.sh again. It deletes everything and starts fresh.
Troubleshooting
docker: permission denied. Your user isn't in the docker group. Run sudo usermod -aG docker $USER, then log out and back in.
The script stops at stage 3 with "MySQL did not become ready". MySQL took longer than 80 seconds to start, which is common on slower machines. Check docker logs docker-db-1, then run the script again.
port is already allocated for port 3000. Another app is using port 3000. Stop that app, or change 127.0.0.1:3000:3000 to 127.0.0.1:3001:3000 in docker/docker-compose.lab.yml and browse to port 3001 instead.
Products show without pictures. Stage 5 couldn't copy the seed images, which is common on macOS and Windows. Run the terminal reset command from the reset section above, or run ./setup-local.sh again. The browser reset won't help here, because it only resets the database, not image files.
After pasting, apply the inline code style (highlight, then press the backtick key) to ./setup-local.sh.
The chatbot says it isn't configured. No Groq API key reached the app. Add your key to .env.lab and run the script again.
/debug/info returns a 404 page. The app container didn't start properly. Run docker logs docker-app-1 and look for the first error.
What to try next
You now have a full vulnerable shop running on your own machine, with nothing exposed to your network. Start with the IDOR findings (PWN-001 to PWN-004), because they only need a browser and a little curiosity about the numbers in the URL. When you're comfortable, try the debug-page-to-admin chain I mentioned earlier.
In my next post, I'll walk through those IDOR findings and show how changing a single number in the URL opens up other people's orders and inboxes. If you get stuck on the setup, open an issue on the Pwnshop repository and I'll take a look.