PHPVulnBank is a security-education artifact. Every vulnerability in it is deliberate, documented and load-bearing. It is not supported software, it has no security patches, and it must never run in production or on any network you do not fully control.
Please do not file vulnerability reports for this repository. Findings from
automated scanners, bug bounty automation and dependency bots are expected and
will be closed. If you believe you have found something that is not in
docs/vulnerabilities.md, that is genuinely
interesting — open a normal issue describing it, and it will likely become a
new lesson.
Two paths give unauthenticated remote code execution, with no credentials and no session required:
| Path | Reach |
|---|---|
POST /api/v2/auth/login with pwd=troy |
The submitted username is executed as a shell command (VULN-02) |
GET|POST /api/v2/tools/exec |
A webshell with no authentication at all (VULN-03) |
There is also an unrestricted file upload that writes attacker-named files into
a PHP-executable directory (VULN-04), arbitrary file read via path traversal
(VULN-07), and SSRF that reaches internal services (VULN-08).
Anyone who can reach this application over the network can run commands on the host that serves it. Treat a running instance as already compromised.
The app port is published on all interfaces, so the lab is reachable from other machines on your network. That is deliberate — a classroom lab that only answers on loopback is not much use.
It also means the section above is not theoretical. Reaching port 8090 is equivalent to shell access on the container. Choose the network accordingly:
| OK | An isolated lab VLAN, a classroom or workshop network, a home LAN you control |
| NEVER | A public IP address |
| NEVER | A cloud VM with an open security group |
| NEVER | A corporate or shared office network |
| NEVER | Behind a port-forwarded router |
| NEVER | Through ngrok, Cloudflare Tunnel, or any similar service |
The container prints this warning, and what an attacker gains, every time it
starts. Bring it down when the session ends — docker compose down. MySQL stays
bound to loopback and is not widened alongside the app.
- Use disposable data. Never point it at a database containing anything
real.
php artisan migrate:fresh --seedrebuilds the entire lab from empty. - Never expose port 80/8090 through a tunnel or reverse proxy, including ngrok-style services for "quick sharing".
- Do not push the container image to a registry without
vulnerablein the tag name, and never tag itlatest. APP_ENV=localandAPP_DEBUG=trueare lab settings. They are intentional here (VULN-23) and must never be copied into a real project.
Two MCP servers ship with this lab (phpvulnbank-api, phpvulnbank-db). They
change the threat model in a way the web app does not, and "bind to
localhost" does not address it — the exposure is not network reachability, it
is client configuration.
MCP servers are normally registered in a developer's personal assistant client, alongside their real tools: filesystem, git, mail, ticketing. The stored prompt-injection payloads in this lab are written specifically to make a model take actions it was not asked to take, and they do not care which tools it has. Connect this server to a session that also holds real ones and the lab's data can reach them.
- Run it in a dedicated client profile with no other MCP servers connected. This is the requirement that matters, and it is unaffected by transport.
- Both servers fail closed: they refuse to start unless
PHPVULNBANK_LAB=1is set deliberately. The guard lives inServer::start(), which both transports call. - Both are registered on both transports:
- stdio (
Mcp::local) — for a student running the whole lab locally. Isolated, and how MCP is normally deployed. - HTTP (
POST /mcp/api,POST /mcp/db) — so a shared classroom instance is usable at all. A student's client cannot launch a stdio subprocess on someone else's machine, so without this the MCP exercises could not be run against a central lab.
- stdio (
- The HTTP endpoints are unauthenticated (
VULN-80). Note this adds little on top of what is already published on the same port:tools/execis an unauthenticated webshell, so the host is already fully compromisable by anyone who can reach it. - On a shared instance, remember
run_queryaccepts free-form SQL on a connection that can drop tables — one student can take the lab down for the class. Rebuild it with:docker compose exec app php artisan migrate:fresh --seed --force run_queryexecutes model-composed SQL on a connection that can drop tables. Never point it at a database holding anything real.- Assume every tool result reaches the model provider. Seed data is synthetic and must stay synthetic.
The lab deliberately keeps weak cryptography (unsalted MD5), disabled framework protections and — in the planned A06 lesson — a dependency with a known advisory. Dependency and code scanners will flag these. Do not merge automated "fixes". Each one silently deletes a lesson, which is the primary failure mode this project guards against.
Every deliberate flaw carries a [VULN-nn: ...] Intentional. Do not "fix"
marker comment at the point of use, and a matching entry in
docs/vulnerabilities.md. The
tests/Feature/Exploits/ suite asserts that vulnerabilities still work — a
failure there means a lesson has been repaired, not that the app is broken.
For issues unrelated to the intentional vulnerabilities — a broken build, a lesson that no longer reproduces, an error in the documentation — open a regular GitHub issue.