MediTrace is a working hospital platform that is broken on purpose. It is a companion lab for the CyberSec University SQL Injection (SQLi) Training Platform at https://cybersec-app.fly.dev. Students learn the theory there and practice the attacks here.
Every feature is genuinely functional (accounts, patient registry, search, lab results, notes, admin tools) - and every OWASP Top 10 flaw is a real, deliberate anti-pattern that works against the app exactly as documented, both by hand and with sqlmap.
Scope: fictional data only, no real patients. Use this only on systems you own. Never run automated scanners against third parties.
Everything runs through one standardized npm interface (Windows, macOS, Linux):
# one-time setup
python -m pip install -r backend/requirements-dev.txt
npm install --prefix frontend
# serve the built app on :8000 and open the browser
npm start
# or: live-reload dev mode (Vite on :5173 proxying /api -> :8000)
npm run start:dev
# run everything in Docker instead
npm run docker:up # http://localhost:8000npm start builds the SPA if needed, starts the FastAPI backend (reusing one
already running on :8000), waits until it is healthy, and opens the browser.
Flags: npm start -- --build forces a rebuild; npm start -- --no-open
skips the browser.
Other scripts: npm run build, npm run lint, npm test,
npm run test:security, npm run test:sqlmap, npm run db:reset,
npm run docker:down.
| Username | Password | Role |
|---|---|---|
admin |
admin123 |
Admin |
doctor |
doctor123 |
Doctor |
nurse |
nurse123 |
Staff |
receptionist |
welcome123 |
Staff |
dr_lee |
lee123 |
Doctor |
dr_chen |
chen123 |
Doctor |
grace |
grace123 |
Patient (Grace Otieno) |
sofia |
sofia123 |
Patient (Sofia Mendez) |
hana |
hana123 |
Patient (Hana Kobayashi) |
Reset the database any time with POST /api/lab/reset (or the Lab page).
Resetting invalidates every session, so sign back in afterwards.
- Log in as
doctor/doctor123- the header shows who you are and your role. - Type
' OR 1=1--into the patient search and search - the whole registry appears. - Open Profile, change the staff ID to
1- the admin's record and SSN. - (Closer) In Admin > Network diagnostics, run
127.0.0.1 | whoami- a shell command executes on the server.
Patient flow: sign in as grace/grace123 to land on My health (own record,
lab results, appointments) and request an appointment with another doctor.
Each step takes one line of input and visibly breaks a "real" system.
| OWASP 2021 | Surface | One-liner |
|---|---|---|
| A03 Injection | login, patient id, search, lab results, appointments | SQLi in 5 flavors: auth bypass, UNION, boolean, error, time |
| A03 Injection | admin diagnostics | ` |
| A01 Broken Access | /api/users/{id}, admin panel, notes |
IDOR + missing role checks + SSN leaks |
| A03 XSS | notes (stored), search (reflected) | unsanitized HTML rendering |
| A10 SSRF | /api/tools/fetch |
fetch any URL, reach internal endpoints |
| A02 Crypto | passwords, sessions | MD5 hashes, predictable tokens, stealable cookie |
| A05 Misconfig | error handler, defaults | verbose SQL errors, documented default creds |
| A04/A09 | login endpoint | no rate limiting, no audit logging |
Each vulnerability is pinned by an automated test that proves it is still exploitable (and therefore cannot be accidentally "fixed" later).
backend/ FastAPI + raw SQLite3
app/main.py app factory, verbose SQL error handler, SPA static serving
app/db.py per-request sqlite3 connections, schema (WAL mode)
app/seed.py deterministic fictional clinic data (incl. patient accounts)
app/vuln/ every deliberate flaw, documented in vuln/README.md
app/routes/ auth, patients, lab, notes, users, portal, admin, tools
tests/
functional/ the app really works
security/ every vulnerability is still exploitable (incl. sqlmap)
frontend/ React + Vite + Tailwind SPA (mobile-first), served by FastAPI
src/auth/ global auth state (AuthProvider + useAuth), RequireAuth guard
src/components/ui/ Button, Card, Badge, Skeleton, Spinner, Alert, EmptyState
src/pages/ registry, patient detail, patient portal, admin, tools, lab
src/**/*.test.tsx Vitest + React Testing Library component tests
package.json root npm scripts - single cross-platform interface (npm start)
scripts/start.mjs zero-dependency launcher: build, port reuse, health poll, browser
Dockerfile multi-stage: node build -> python runtime
docker-compose.yml local lab
fly.toml Fly.io deployment
.github/workflows/ CI: backend tests + sqlmap integration + frontend test/lint/build
Key design decisions:
- Raw SQL, not an ORM - ORMs parameterize by default and would silently
"fix" the SQLi. Vulnerable queries live in
app/vuln/sqli.py; the safe counterparts elsewhere use?placeholders, so the fix is a visible diff. - Auth-gated data surfaces - patient/lab data now require a session; the auth-bypass login payload is the zero-knowledge way in. See the cheat sheet.
- sqlmap compatibility - injectable params are typed
str, error messages leak the executed SQL (error-based), result counts are a stable boolean truth signal, and the canonical SQLite heavy-query time payload is documented. - Two test suites - functional tests prove the app works; security
regression tests prove the vulns still work.
pytest tests/securitygates the lab. The frontend is covered by Vitest component tests.
npm test # backend tests + frontend unit tests + build
npm run test:security # vuln regression only
npm run test:sqlmap # needs sqlmap installedEquivalently from backend/: python -m pytest (everything),
python -m pytest tests/security (vuln regression), and
python -m pytest tests/security/test_sqlmap.py -m sqlmap. Two sqlmap tests
pin the student path: one runs stock sqlmap against the public login
endpoint with no cookie (--level=2 --tables, the beginner command) and
asserts the tables are enumerated; the other logs in via requests, passes
sqlmap --cookie=meditrace_session=..., and dumps the UNION surface.
The beginner one-liner (no setup, matches the browser bypass):
sqlmap -u "http://localhost:8000/api/auth/login?username=admin&password=x" \
--level=2 --batch --tablesThat URL is the GET debug variant of the login endpoint: it takes
credentials as query params and returns 200 with {"error": ...} on failure
instead of 401, so sqlmap does not abort. --level=2 is required because the
login query has a trailing AND password_hash='...' clause, and the payload +
boundary sqlmap uses to neutralize it (SQLite JSON boolean-CASE + a
string-closing AND 'x'='x') are both tagged level 2 in sqlmap's data. The
full catalog and every working command live in backend/app/vuln/README.md.
From frontend/: npm test runs the Vitest + React Testing Library suite
(auth state, layout identity, page loading/error/empty states).
Fly.io (matches the companion app's host):
fly launch --no-deploy
fly deployThe Dockerfile builds the SPA and serves it from FastAPI as one artifact, so
/ and /patients/1 both work with no CORS setup. SQLite persists on the
meditrace_data volume; without a volume it simply reseeds on every deploy
(which is fine for a lab).
- Backend: SQLi core (auth, UNION, boolean, error, time) + tests
- Backend: XSS, IDOR, command injection, SSRF + tests
- Frontend: responsive SPA
- Frontend: Vitest + React Testing Library component tests (15 passing)
- Frontend: role-aware UI, auth guard, patient portal ("My health")
- Backend: auth-gated data surfaces + patient BOLA (appointment booking)
- Deployment: Docker, compose, Fly.io, CI
- Audit challenge tracker (gamified "find all N findings")
-
SAFE_MODEtoggle (same app, fixed queries) for before/after teaching