xss_lab.py — Source Code

Back Download
/labs/xss_lab.py
#!/usr/bin/env python3
"""
XSS Lab - Cross-Site Scripting Practice
The Hacker's Arsenal - Lesson 4

Multiple XSS challenges with varying difficulty:
1. No filter (easy)
2. Basic filter - blocks <script> tags
3. Better filter - blocks multiple patterns
4. Stored XSS - comment section
5. DOM-based XSS - welcome page

Run this script and open http://localhost:8888 in your browser.

WARNING: This code is INTENTIONALLY VULNERABLE for educational purposes.
"""

from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

COMMENTS = []

HTML_PAGE = '''<!DOCTYPE html>
<html>
<head>
    <title>XSS Lab</title>
    <style>
        body {{ font-family: Arial; max-width: 800px; margin: 50px auto; background: #1a1a1a; color: #e0e0e0; }}
        h1 {{ color: #6EFF24; }}
        .section {{ background: #252525; padding: 20px; margin: 20px 0; border-radius: 8px; }}
        input, textarea {{ width: 100%; padding: 10px; margin: 10px 0; background: #333; border: 1px solid #6EFF24; color: white; }}
        button {{ background: #6EFF24; color: black; padding: 10px 20px; border: none; cursor: pointer; font-weight: bold; }}
        .comment {{ background: #333; padding: 10px; margin: 10px 0; border-left: 3px solid #6EFF24; }}
        .vulnerable {{ border-left-color: red; }}
        code {{ background: #333; padding: 2px 6px; color: #6EFF24; }}
    </style>
</head>
<body>
    <h1>XSS Practice Lab</h1>

    <div class="section">
        <h2>1. Reflected XSS (No Filter)</h2>
        <form action="/search1" method="GET">
            <input type="text" name="q" placeholder="Search...">
            <button type="submit">Search</button>
        </form>
        {search1_result}
    </div>

    <div class="section">
        <h2>2. Reflected XSS (Basic Filter)</h2>
        <p><em>Blocks: &lt;script&gt; tags</em></p>
        <form action="/search2" method="GET">
            <input type="text" name="q" placeholder="Search...">
            <button type="submit">Search</button>
        </form>
        {search2_result}
    </div>

    <div class="section">
        <h2>3. Reflected XSS (Better Filter)</h2>
        <p><em>Blocks: &lt;script&gt;, &lt;img&gt;, onerror, onload</em></p>
        <form action="/search3" method="GET">
            <input type="text" name="q" placeholder="Search...">
            <button type="submit">Search</button>
        </form>
        {search3_result}
    </div>

    <div class="section">
        <h2>4. Stored XSS (Comment Section)</h2>
        <form action="/comment" method="POST">
            <input type="text" name="name" placeholder="Your name">
            <textarea name="comment" placeholder="Your comment"></textarea>
            <button type="submit">Post Comment</button>
        </form>
        <h3>Comments:</h3>
        {comments}
    </div>

    <div class="section">
        <h2>5. DOM-Based XSS</h2>
        <p>Check out our <a href="/welcome#Guest">welcome page</a></p>
        <p><em>Hint: The welcome page uses JavaScript to read the URL fragment</em></p>
    </div>

    <div class="section vulnerable">
        <h2>Challenge Mode</h2>
        <p>Can you trigger XSS in all 5 sections?</p>
    </div>
</body>
</html>'''

WELCOME_PAGE = '''<!DOCTYPE html>
<html>
<head>
    <title>Welcome</title>
    <style>
        body {{ font-family: Arial; max-width: 600px; margin: 100px auto; text-align: center; background: #1a1a1a; color: #e0e0e0; }}
        h1 {{ color: #6EFF24; }}
    </style>
</head>
<body>
    <h1>Welcome!</h1>
    <p id="greeting">Loading...</p>
    <script>
        // DOM XSS Vulnerability - reads from URL fragment without sanitization
        var name = decodeURIComponent(location.hash.slice(1));
        document.getElementById('greeting').innerHTML = 'Hello, ' + name + '!';
    </script>
    <p><a href="/" style="color: #6EFF24;">Back to Lab</a></p>
</body>
</html>'''


class XSSLabHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        params = parse_qs(parsed.query)

        search1_result = ''
        search2_result = ''
        search3_result = ''

        if parsed.path == '/search1':
            q = params.get('q', [''])[0]
            # No filter - directly vulnerable
            search1_result = f'<p>Results for: {q}</p>'

        elif parsed.path == '/search2':
            q = params.get('q', [''])[0]
            # Basic filter - blocks <script> tags
            filtered = q.replace('<script>', '').replace('</script>', '')
            search2_result = f'<p>Results for: {filtered}</p>'

        elif parsed.path == '/search3':
            q = params.get('q', [''])[0]
            # Better filter - blocks common payloads
            blocked = ['<script', '<img', 'onerror', 'onload', 'javascript:']
            filtered = q
            for term in blocked:
                filtered = filtered.lower().replace(term.lower(), '')
            search3_result = f'<p>Results for: {filtered}</p>'

        elif parsed.path == '/welcome':
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(WELCOME_PAGE.encode())
            return

        # Build comments HTML
        comments_html = ''
        for c in COMMENTS:
            comments_html += f'<div class="comment"><strong>{c["name"]}</strong>: {c["comment"]}</div>'
        if not comments_html:
            comments_html = '<p><em>No comments yet</em></p>'

        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

        page = HTML_PAGE.format(
            search1_result=search1_result,
            search2_result=search2_result,
            search3_result=search3_result,
            comments=comments_html
        )
        self.wfile.write(page.encode())

    def do_POST(self):
        parsed = urlparse(self.path)

        if parsed.path == '/comment':
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length).decode()
            params = parse_qs(post_data)

            name = params.get('name', ['Anonymous'])[0]
            comment = params.get('comment', [''])[0]

            # Stored XSS - comments are saved and displayed to everyone
            COMMENTS.append({'name': name, 'comment': comment})

        # Redirect back to main page
        self.send_response(302)
        self.send_header('Location', '/')
        self.end_headers()

    def log_message(self, format, *args):
        print(f"[{args[0]}] {self.path}")


if __name__ == '__main__':
    server = HTTPServer(('localhost', 8888), XSSLabHandler)
    print("=" * 50)
    print("  XSS LAB RUNNING")
    print("  Open http://localhost:8888 in your browser")
    print("=" * 50)
    server.serve_forever()