|
| 1 | +# Architecture |
| 2 | + |
| 3 | +How PHPVulnBank is put together, and where the deliberate flaws live. |
| 4 | + |
| 5 | +Diagrams are Mermaid and render on GitHub. Companion documents: |
| 6 | +[`vulnerabilities.md`](vulnerabilities.md) (the lesson catalogue), |
| 7 | +[`api-refactor.md`](api-refactor.md) (why it is API-first), |
| 8 | +[`mcp-design.md`](mcp-design.md) (the MCP layer), |
| 9 | +[`../SECURITY.md`](../SECURITY.md) (how to run it safely). |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## 1. System overview |
| 14 | + |
| 15 | +The API is the **system of record**. All business logic — and therefore every |
| 16 | +server-side vulnerability — sits behind `/api/v2/*`. The browser client holds |
| 17 | +none of it. |
| 18 | + |
| 19 | +```mermaid |
| 20 | +flowchart TB |
| 21 | + subgraph CLIENTS["Clients"] |
| 22 | + BROWSER["Browser<br/><i>thin Blade shells</i>"] |
| 23 | + TOOLS["curl · Postman · Burp<br/><i>OpenAPI spec available</i>"] |
| 24 | + MCPC["MCP client<br/><i>stdio or HTTP</i>"] |
| 25 | + end |
| 26 | +
|
| 27 | + subgraph APP["Laravel 13 application"] |
| 28 | + direction TB |
| 29 | +
|
| 30 | + subgraph ROUTES["Routing"] |
| 31 | + WEB["web.php<br/>views only, no logic"] |
| 32 | + API["api.php<br/><b>/api/v2/* — system of record</b>"] |
| 33 | + AI["ai.php<br/>MCP servers"] |
| 34 | + end |
| 35 | +
|
| 36 | + MW["Middleware<br/>VulnModeBanner · cookies · session<br/><b>no VerifyCsrfToken on api</b>"] |
| 37 | +
|
| 38 | + CTRL["Controllers<br/>Auth · Account · Transfer · Feedback<br/>Admin · Kyc · Register · Utility · OpenApi"] |
| 39 | +
|
| 40 | + LQ["<b>LegacyQuery</b><br/><i>single SQLi chokepoint</i>"] |
| 41 | + MODELS["Models<br/>User → banktable · Transaction · AuditLog"] |
| 42 | + end |
| 43 | +
|
| 44 | + DB[("MySQL<br/>bankdb")] |
| 45 | +
|
| 46 | + BROWSER --> WEB |
| 47 | + WEB -. "fetch()" .-> API |
| 48 | + TOOLS --> API |
| 49 | + MCPC --> AI |
| 50 | +
|
| 51 | + API --> MW --> CTRL |
| 52 | + AI --> CTRL |
| 53 | +
|
| 54 | + CTRL --> LQ |
| 55 | + CTRL --> MODELS |
| 56 | + LQ --> DB |
| 57 | + MODELS --> DB |
| 58 | +
|
| 59 | + classDef vuln fill:#b30000,stroke:#600,color:#fff |
| 60 | + class LQ,MW vuln |
| 61 | +``` |
| 62 | + |
| 63 | +**Why the client is deliberately thin and framework-free.** Moving behind a JSON |
| 64 | +API removed the server-side XSS sink — `application/json` does not execute — so |
| 65 | +the vulnerability class *moved* to DOM-based rather than disappearing. It now |
| 66 | +lives in one `render()` helper in `layouts/app.blade.php` that writes API data |
| 67 | +with `innerHTML`. A React or Vue client would auto-escape by default, meaning the |
| 68 | +XSS lessons would have to be fought back in, and a bundler would hide the sink. |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## 2. Where the vulnerabilities concentrate |
| 73 | + |
| 74 | +Not evenly spread. Four chokepoints carry most of them, which is what makes the |
| 75 | +repository auditable. |
| 76 | + |
| 77 | +```mermaid |
| 78 | +flowchart LR |
| 79 | + subgraph SERVER["Server side — survives any client"] |
| 80 | + LQ["LegacyQuery<br/>VULN-01 05 06"] |
| 81 | + SHELL["UtilityController<br/>VULN-03 08"] |
| 82 | + UP["KycController<br/>VULN-04"] |
| 83 | + AUTHZ["missing checks<br/>VULN-11 12"] |
| 84 | + end |
| 85 | +
|
| 86 | + subgraph CLIENT["Browser-dependent — needs the Blade client"] |
| 87 | + DOM["render() innerHTML<br/>VULN-13 14"] |
| 88 | + CSRF["form-encoded + cookie<br/>VULN-10"] |
| 89 | + HDRS["no CSP / X-Frame-Options<br/>VULN-40"] |
| 90 | + end |
| 91 | +
|
| 92 | + subgraph MCPL["MCP-only — no web analogue"] |
| 93 | + POISON["tool #Description<br/>VULN-75"] |
| 94 | + T2SQL["run_query<br/>VULN-90 91"] |
| 95 | + DEPUTY["shared credential<br/>VULN-81"] |
| 96 | + end |
| 97 | +
|
| 98 | + classDef vuln fill:#b30000,stroke:#600,color:#fff |
| 99 | + class LQ,SHELL,UP,AUTHZ,DOM,CSRF,HDRS,POISON,T2SQL,DEPUTY vuln |
| 100 | +``` |
| 101 | + |
| 102 | +Two framework defaults had to be **opted out of** deliberately, both documented |
| 103 | +in `bootstrap/app.php`: |
| 104 | + |
| 105 | +| Default | Why it was removed | |
| 106 | +|---|---| |
| 107 | +| `VerifyCsrfToken` on `api` | Needed for `VULN-10`. Also requires form-encoded acceptance — a JSON-only API is not CSRF-able at all | |
| 108 | +| `TrimStrings` | Not a security control, but it strips the trailing space from `' or '1'='1' -- `, and MySQL only treats `--` as a comment when followed by whitespace | |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## 3. The MCP layer — the A/B contrast |
| 113 | + |
| 114 | +Two servers, deliberately. The point is not either one alone, it is the |
| 115 | +difference between them. |
| 116 | + |
| 117 | +```mermaid |
| 118 | +flowchart LR |
| 119 | + CLIENT["MCP client"] |
| 120 | +
|
| 121 | + subgraph SRV["MCP servers"] |
| 122 | + APISRV["<b>phpvulnbank-api</b><br/>8 tools<br/><i>via application layer</i>"] |
| 123 | + DBSRV["<b>phpvulnbank-db</b><br/>4 tools<br/><i>direct connection</i>"] |
| 124 | + end |
| 125 | +
|
| 126 | + GUARD["McpGuard<br/><i>fails closed without<br/>PHPVULNBANK_LAB=1</i>"] |
| 127 | +
|
| 128 | + CONTROLS["Application layer<br/>authorisation · validation<br/>masking · <b>audit_logs</b>"] |
| 129 | +
|
| 130 | + DB[("MySQL<br/><i>groot: ALL PRIVILEGES</i>")] |
| 131 | +
|
| 132 | + CLIENT --> GUARD --> APISRV |
| 133 | + GUARD --> DBSRV |
| 134 | + APISRV --> CONTROLS --> DB |
| 135 | + DBSRV == "bypasses everything" ==> DB |
| 136 | +
|
| 137 | + classDef vuln fill:#b30000,stroke:#600,color:#fff |
| 138 | + class DBSRV vuln |
| 139 | +``` |
| 140 | + |
| 141 | +Run the same action through each, then diff `audit_logs`: one leaves a trail, |
| 142 | +the other leaves nothing (`VULN-92`). Every control this application implements |
| 143 | +lives in the application layer, and a tool that opens its own connection |
| 144 | +discards all of it at once while looking like a sensible latency decision. |
| 145 | + |
| 146 | +Both transports are registered. `stdio` for a student running the lab locally; |
| 147 | +HTTP (`POST /mcp/api`, `POST /mcp/db`) so a shared classroom instance is usable |
| 148 | +at all, since a client cannot launch a subprocess on someone else's machine. |
| 149 | +The HTTP endpoints are unauthenticated — `VULN-80`. |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## 4. Deployment |
| 154 | + |
| 155 | +```mermaid |
| 156 | +flowchart TB |
| 157 | + subgraph HUB["Docker Hub"] |
| 158 | + IMG["krishnapadala55/phpvulnbank<br/>laravel-bundled-vulnerable-1.0"] |
| 159 | + end |
| 160 | +
|
| 161 | + subgraph COMPOSE["Compose — primary path"] |
| 162 | + APPC["app<br/>php:8.3-apache"] |
| 163 | + MYC[("mysql:8.4<br/><i>127.0.0.1 only</i>")] |
| 164 | + APPC --- MYC |
| 165 | + end |
| 166 | +
|
| 167 | + subgraph BUNDLED["Bundled — one command"] |
| 168 | + ONE["Apache + PHP + MariaDB<br/>in one container"] |
| 169 | + end |
| 170 | +
|
| 171 | + LAN["LAN / WireGuard<br/>port 8090, all interfaces"] |
| 172 | +
|
| 173 | + IMG -.->|docker run| BUNDLED |
| 174 | + COMPOSE --> LAN |
| 175 | + BUNDLED --> LAN |
| 176 | +
|
| 177 | + classDef warn fill:#b30000,stroke:#600,color:#fff |
| 178 | + class LAN warn |
| 179 | +``` |
| 180 | + |
| 181 | +`migrate:fresh --seed` rebuilds the whole lab from empty on every start, so a |
| 182 | +container is disposable and a student who drops the database costs one command. |
| 183 | + |
| 184 | +**Reaching port 8090 is equivalent to shell access on the container** — two |
| 185 | +unauthenticated RCE paths (`VULN-02`, `VULN-03`) plus unauthenticated SQL over |
| 186 | +MCP. Isolated lab network only. MySQL stays bound to loopback and is not |
| 187 | +widened alongside the app. |
| 188 | + |
| 189 | +--- |
| 190 | + |
| 191 | +## 5. CI |
| 192 | + |
| 193 | +```mermaid |
| 194 | +flowchart LR |
| 195 | + PUSH["push / PR"] |
| 196 | +
|
| 197 | + TESTS["<b>tests.yml</b><br/>82 tests<br/><i>THE GATE</i>"] |
| 198 | + SAST["sast.yml<br/>Semgrep, legacy tag vs laravel/app/"] |
| 199 | + DAST["dast.yml<br/>ZAP, manual + weekly"] |
| 200 | +
|
| 201 | + PUSH --> TESTS |
| 202 | + PUSH --> SAST |
| 203 | + DAST |
| 204 | +
|
| 205 | + classDef gate fill:#0a6,stroke:#064,color:#fff |
| 206 | + class TESTS gate |
| 207 | +``` |
| 208 | + |
| 209 | +**`tests.yml` is the only gate**, and it is inverted from the usual direction: |
| 210 | +most of the suite asserts that vulnerabilities **still work**. A failure means a |
| 211 | +lesson has been silently repaired — by a framework upgrade, a linter, or a |
| 212 | +well-meaning contributor — which is this project's primary risk. |
| 213 | + |
| 214 | +The scanners are teaching material, not gates. `sast.yml` scans the legacy tree |
| 215 | +and `laravel/app/` **separately** and reports the delta: same 28 vulnerabilities, |
| 216 | +far fewer findings, because Semgrep recognises Eloquent and Blade as safe. The |
| 217 | +legacy tree is no longer in the working directory — the workflow materialises it |
| 218 | +from the `legacy-flat-php` tag, so that tag must not be deleted. `dast.yml` frames its output as a coverage gap — a |
| 219 | +scanner finds reflected XSS and missing headers, not the IDOR, the |
| 220 | +negative-amount transfer, the race condition, or the `troy` backdoor. |
| 221 | + |
| 222 | +CodeQL is not used: **it does not support PHP.** |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## 6. Repository layout |
| 227 | + |
| 228 | +``` |
| 229 | +├── laravel/ the application |
| 230 | +│ ├── app/ |
| 231 | +│ │ ├── Http/Controllers/Api/V2/ all business logic |
| 232 | +│ │ ├── Mcp/{Servers,Tools}/ MCP layer |
| 233 | +│ │ ├── Models/ User → banktable, Transaction, AuditLog |
| 234 | +│ │ └── Support/ LegacyQuery, McpGuard, OpenApiSpec |
| 235 | +│ ├── resources/views/ thin Blade shells + the innerHTML render helper |
| 236 | +│ ├── routes/ web.php · api.php · ai.php |
| 237 | +│ ├── tests/Feature/Exploits/ asserts vulnerabilities still work |
| 238 | +│ ├── Dockerfile compose variant |
| 239 | +│ └── Dockerfile.bundled single-container variant |
| 240 | +├── payload/csrf/ CSRF proof of concept |
| 241 | +├── docs/ this file and the design documents |
| 242 | +└── SECURITY.md read before running |
| 243 | +``` |
| 244 | + |
| 245 | +Removed in July 2026: the legacy application (`src/`), its build scaffolding |
| 246 | +(root `Dockerfile`, `dock/`, `dbscript/`), the Jenkins and Azure pipelines, the |
| 247 | +`DevSecOpS/` scan scripts, and the unused Vite/npm chain. |
| 248 | + |
| 249 | +The legacy application is tagged **`legacy-flat-php`** — `git checkout |
| 250 | +legacy-flat-php` retrieves it, and `sast.yml` materialises it from there for the |
| 251 | +comparison above. The published legacy Docker images are self-contained and |
| 252 | +still run. |
0 commit comments