An educational project demonstrating Cross-Site Request Forgery (CSRF) attacks and how to prevent them using Double Submit Cookie pattern.
Built with Angular (frontend) and NestJS (backend).
CSRF is an attack where a malicious website tricks a user's browser into making an unwanted request to a trusted site where the user is authenticated. Because browsers automatically send cookies with requests, the server can't distinguish between legitimate and forged requests.
In vulnerable mode (CSRF_MODE=vuln):
- The server relies solely on the session cookie for authentication
- The browser sends this cookie with every request to the server — including requests initiated by other websites
- An attacker page can submit a form to the server, and the browser will attach the session cookie automatically
- The server sees a valid session cookie and processes the request as if the user made it
In secure mode (CSRF_MODE=safe):
- On login, the server generates a random CSRF token and stores it in:
- A JS-readable cookie (
XSRF-TOKEN) - The server-side session
- A JS-readable cookie (
- For every state-changing request (POST), the client must send the token in the
X-XSRF-TOKENheader - The server compares the header value with the session-stored value
- The attacker page cannot read the
XSRF-TOKENcookie (same-origin policy prevents cross-origin cookie reading) - A plain form submission cannot set custom headers — so the attacker can't provide the required
X-XSRF-TOKENheader - The server rejects the request with 403 Forbidden
- Node.js >= 20.19 (use
nvm use 22if needed)
# Install all dependencies
cd backend && npm install && cd ../frontend && npm install && cd ..Open three terminals:
# Terminal 1 — Backend (vulnerable)
cd backend && CSRF_MODE=vuln npx nest start --watch
# Terminal 2 — Frontend
cd frontend && npx ng serve
# Terminal 3 — Attacker page
npx http-server attacker -p 4444 -c-1 --corsSame as above, but change the backend command:
# Terminal 1 — Backend (secure)
cd backend && CSRF_MODE=safe npx nest start --watch- Open http://localhost:4200 — the Angular app
- Log in as alice (password:
password) - Note the balance: $1000
- Open http://localhost:4444 in another tab — the attacker page
- Click "Claim Prize"
- Go back to the Angular app and click "Refresh"
| Mode | What happens | Balance after attack |
|---|---|---|
vuln |
Transfer succeeds silently | $500 (lost $500 to bob) |
safe |
Server returns 403 Forbidden | $1000 (unchanged) |
Check the backend terminal for logs showing whether the request was processed or blocked.
csrf-demo/
├── backend/ NestJS API (port 3000)
│ └── src/
│ ├── main.ts Bootstrap, CORS, cookie-parser
│ ├── store.ts In-memory users & sessions
│ ├── auth/
│ │ ├── auth.module.ts
│ │ └── auth.controller.ts Login/logout endpoints
│ ├── account/
│ │ ├── account.module.ts
│ │ └── account.controller.ts Balance/transfer endpoints
│ └── csrf/
│ └── csrf.guard.ts CSRF token validation guard
├── frontend/ Angular SPA (port 4200)
│ └── src/app/
│ ├── app.ts Root component
│ ├── app.config.ts HttpClient + XSRF config
│ ├── api.service.ts API client
│ ├── login/ Login component
│ └── dashboard/ Dashboard component
├── attacker/ Malicious page (port 4444)
│ └── index.html Auto-submitting CSRF form
└── README.md
| Method | URL | Description |
|---|---|---|
| POST | /auth/login | Login (sets session cookie) |
| POST | /auth/logout | Logout (clears cookies) |
| GET | /account/balance | Get current user's balance |
| POST | /account/transfer | Transfer money (CSRF-protected in safe mode) |