Listen to this Post

Introduction:
Stored Cross-Site Scripting (XSS) remains one of the most pervasive web application vulnerabilities, allowing attackers to inject malicious scripts into web pages viewed by other users. When application developers fail to properly validate file uploads—particularly in markdown-supported content management systems—they inadvertently open the door to complete account compromise and data theft. This article explores a real-world Stored XSS vulnerability discovered through unvalidated file extension handling in an image upload feature, demonstrating how a simple oversight can lead to devastating consequences.
Learning Objectives:
- Understand the root causes of Stored XSS vulnerabilities in file upload functionality
- Learn how to identify and exploit unvalidated markdown insertion points
- Master mitigation techniques including input validation, content-type verification, and Content Security Policy implementation
You Should Know:
- Understanding the Vulnerability: Unvalidated File Extensions in Image Uploads
The discovered vulnerability stemmed from a classic misconfiguration: the application expected only image file extensions (.jpeg, .png, .bmp) but failed to enforce this restriction on the server-side. Instead, the system accepted all file extensions, including .html files that could contain malicious JavaScript payloads.
When combined with markdown rendering that processes user-submitted content without proper sanitization, this creates a potent attack vector. The markdown parser would interpret HTML files as legitimate content, executing any embedded scripts when the page loaded for other users.
Step-by-Step Guide to Reproduce the Vulnerability:
- Identify Image Upload Functionality: Locate any section in the web application that allows file uploads, particularly within markdown-supported content editors (e.g., blog posts, comments, profile pictures).
-
Test File Extension Restrictions: Attempt to upload a harmless HTML file named `test.html` containing simple content:
</p></li> </ol> <h1>Test</h1> <p>
- Analyze Server Response: If the upload succeeds without rejection, the validation is likely client-side only or completely absent.
-
Craft the Malicious Payload: Create an HTML file (
bypass.html) containing:<script>alert('XSS')</script></p></li> </ol> <h1>You've been hacked!</h1> <p>- Use Data URI for Direct Injection: In cases where direct HTML upload is blocked, try the data URI format:
data:text/html,<script>alert(123)</script><br></li> </ol> <h1>You've got hacked!</h1>
- Upload the Payload: Submit the crafted file through the upload interface.
-
Access the Uploaded Content: Navigate to the location where the uploaded content is rendered (e.g., profile page, gallery, or post).
-
Trigger the XSS: If the application doesn’t sanitize the output, the script executes in the browser context of every visitor viewing that page.
Command Line Tools for Testing File Upload Validation:
Linux:
Test file upload with cURL curl -X POST -F "[email protected]" https://target.com/upload Check server response headers for validation curl -I https://target.com/uploads/test.html Use Burp Suite or OWASP ZAP for advanced testing
Windows (PowerShell):
Basic file upload test Invoke-WebRequest -Uri https://target.com/upload -Method POST -Form @{file=Get-Item .\test.html} Check content-type validation Invoke-WebRequest -Uri https://target.com/uploads/test.html -Method HEADPython Script for Automated Testing:
import requests def test_file_upload(url, file_path): files = {'file': (file_path, open(file_path, 'rb'), 'text/html')} response = requests.post(url, files=files) if response.status_code == 200: print(f"[+] File uploaded successfully: {file_path}") print(f" Response: {response.text[:200]}") else: print(f"[-] Upload failed: {response.status_code}") Usage test_file_upload('https://target.com/upload', 'bypass.html')2. Server-Side Validation: The First Line of Defense
The cornerstone of preventing file upload vulnerabilities is implementing robust server-side validation. Client-side checks are easily bypassed using tools like Burp Suite or browser developer tools.
Step-by-Step Guide to Implement Proper Validation:
- Validate File Extension: Check the file extension against an allowlist of permitted types:
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif'}</li> </ol> def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[bash].lower() in ALLOWED_EXTENSIONS- Verify MIME Type: Use server-side libraries to check the actual MIME type:
import magic</li> </ol> def validate_mime(file_content): mime = magic.from_buffer(file_content, mime=True) return mime in {'image/jpeg', 'image/png', 'image/bmp', 'image/gif'}- Perform Image Magic Validation: Use tools like `imagemagick` to verify the file is a genuine image:
Linux command to validate image integrity identify -verbose uploaded_image.jpg If the command fails, the file is not a valid image
-
Rename Uploaded Files: Store files with randomly generated names and remove extensions:
import uuid import os</p></li> </ol> <p>def secure_filename(original_name): ext = os.path.splitext(original_name)[bash] if ext in ALLOWED_EXTENSIONS: return str(uuid.uuid4()) + ext return None
- Set Proper File Permissions: Ensure uploaded files are not executable:
Linux file permission hardening chmod 644 /var/www/uploads/ chown www-data:www-data /var/www/uploads/ Prevent script execution in upload directory echo "Options -ExecCGI" > /var/www/uploads/.htaccess
-
Implement Content Security Policy (CSP) Headers: Add CSP headers to prevent inline script execution:
Apache .htaccess configuration Header set Content-Security-Policy "default-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'unsafe-inline';"
3. Markdown Sanitization: Beyond File Validation
When applications allow markdown with embedded HTML, additional sanitization is critical. Even with proper file validation, markdown parsers can be exploited through other means.
Step-by-Step Guide to Markdown Security:
- Choose a Secure Markdown Parser: Use sanitized libraries like `cmark-gfm` (GitHub Flavored Markdown) which strips dangerous HTML:
import cmarkgfm Render markdown safely safe_html = cmarkgfm.github_flavored_markdown_to_html(user_input)
-
Implement HTML Sanitization: Use `bleach` or `html-sanitizer` to filter allowed tags:
import bleach</p></li> </ol> <p>ALLOWED_TAGS = ['h1', 'h2', 'h3', 'p', 'a', 'ul', 'ol', 'li', 'strong', 'em', 'img'] ALLOWED_ATTRIBUTES = {'a': ['href'], 'img': ['src', 'alt']} def sanitize_markdown(content): html = cmarkgfm.markdown_to_html(content) return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)- Disable Dangerous Features: Disallow `javascript:` URIs and `data:` URIs in markdown:
import re</li> </ol> def sanitize_uri(uri): Block data: URIs and javascript: URIs if re.match(r'^data:text/html', uri): return None if re.match(r'^javascript:', uri): return None return uri
- Test Markdown Rendering Locally: Set up a local testing environment to verify sanitization:
Install markdown security tools pip install bleach cmarkgfm Test with known XSS payloads python -c "import cmarkgfm, bleach; print(bleach.clean(cmarkgfm.markdown_to_html('<script>alert(1)</script>')))"
4. Client-Side Protections: Layered Defense
While server-side validation is paramount, client-side protections serve as an additional layer against accidental submissions and user convenience.
Step-by-Step Guide to Client-Side Implementation:
1. Implement File Input Restrictions in HTML:
<input type="file" accept="image/png, image/jpeg, image/bmp, image/gif" required>
2. Add JavaScript Validation:
document.getElementById('fileInput').addEventListener('change', function(e) { const allowedExtensions = ['jpg', 'jpeg', 'png', 'bmp', 'gif']; const file = this.files[bash]; const extension = file.name.split('.').pop().toLowerCase(); if (!allowedExtensions.includes(extension)) { alert('Invalid file type. Please upload an image.'); this.value = ''; return false; } });3. Use Content-Type Check in JavaScript:
function validateFile(file) { const allowedTypes = ['image/jpeg', 'image/png', 'image/bmp', 'image/gif']; if (!allowedTypes.includes(file.type)) { alert('Invalid file type detected.'); return false; } return true; }5. Advanced Exploitation: Bypassing Weak Protections
Attackers often find creative ways to bypass basic security measures. Understanding these techniques is crucial for effective defense.
Common Bypass Techniques:
- Double Extension: Upload `malicious.php.jpg` to bypass extension checks.
-
Null Byte Injection: Use `malicious.php%00.jpg` to trick validation logic.
-
MIME Type Spoofing: Change the content-type header in intercepted requests:
Using Burp Suite or OWASP ZAP Set Content-Type: image/jpeg in the request
-
File Signature Tampering: Add image magic bytes before malicious code:
Add JPEG header to any file echo -e "\xFF\xD8\xFF\xE0" > bypass.jpg cat bypass.html >> bypass.jpg Resulting file has JPEG signature but contains HTML
6. Hardening Cloud and API Endpoints
Modern applications often use cloud storage (S3, Azure Blob, GCS) and API endpoints for file uploads. These require specific hardening measures.
Cloud Storage Security Configuration:
1. AWS S3 Bucket Policies:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/", "Condition": { "StringNotEquals": { "s3:Content-Type": ["image/jpeg", "image/png", "image/bmp"] } } } ] }2. Restrict File Types in API Gateway:
AWS Lambda Authorizer def lambda_handler(event, context): content_type = event['headers'].get('Content-Type', '') if content_type not in ['image/jpeg', 'image/png', 'image/bmp']: return { 'statusCode': 400, 'body': json.dumps({'error': 'Invalid file type'}) }3. CDN Security Configuration:
Cloudflare Workers to block malicious uploads addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function handleRequest(request) { const contentType = request.headers.get('content-type') if (contentType && !['image/jpeg','image/png','image/bmp'].includes(contentType)) { return new Response('Invalid file type', { status: 400 }) } return fetch(request) }7. Monitoring and Incident Response
After implementing security measures, continuous monitoring is essential to detect and respond to attempted exploits.
Step-by-Step Monitoring Setup:
1. Implement File Upload Logging:
import logging logging.basicConfig(filename='upload_audit.log', level=logging.INFO) def log_upload(user_id, filename, ip, success): logging.info(f"USER:{user_id} FILE:{filename} IP:{ip} SUCCESS:{success}")2. Set Up Alerting Mechanisms:
Linux script to monitor log files for suspicious activity tail -f /var/log/apache2/access.log | grep -E ".(html|php|js|phtml)" | mail -s "Suspicious Upload" [email protected]
3. Implement Rate Limiting on Upload Endpoints:
Nginx rate limiting location /upload { limit_req zone=upload_zone burst=5 nodelay; proxy_pass http://backend; }4. Configure Web Application Firewall (WAF):
ModSecurity rule to block data: URIs SecRule REQUEST_FILENAME "upload" "phase:2,id:100001,deny,msg:'Data URI block',chain" SecRule ARGS "@contains data:text/html" "chain"
5. Regular Security Audits:
Automate vulnerability scanning with Nikto nikto -h https://target.com -o report.html Use OWASP ZAP for API endpoint scanning zap-cli open-url https://target.com
What Undercode Say:
- Key Takeaway 1: The discovery of Stored XSS through unvalidated image uploads in under 5 minutes highlights how even simple oversights can lead to critical vulnerabilities. The root cause—failure to filter file extensions on the server-side—is a fundamental security flaw that persists in many applications today.
-
Key Takeaway 2: Proper validation is not just about file extensions but includes MIME type verification, image magic validation, content sanitization, and strict CSP policies. A multi-layered defense strategy is essential to prevent such attacks from succeeding.
Analysis: The vulnerability described demonstrates the dangerous intersection of file upload functionality and markdown rendering. When applications accept markdown with embedded HTML, they must rigorously sanitize all input, not just file uploads. The attacker’s use of a data URI payload shows how modern browsers can execute scripts from unexpected sources. Organizations must move beyond client-side validation and implement comprehensive server-side security measures. The rapid discovery time (5 minutes) indicates that automated scanners can easily detect such issues, but manual testing remains crucial for nuanced vulnerabilities. The presence of stored XSS means that once injected, the attack persists indefinitely until fixed—potentially compromising every subsequent visitor. This case study reinforces the importance of secure coding practices and regular security assessments for all web applications, regardless of their perceived risk level.
Prediction:
- -1: The increasing reliance on markdown in content management systems will lead to a surge in similar vulnerabilities, with attackers automating detection of unvalidated file upload endpoints using AI-powered scanning tools.
- -1: Organizations that fail to implement comprehensive validation frameworks will face data breaches, reputation damage, and regulatory fines as Stored XSS attacks escalate in sophistication.
- +1: The growing awareness of such vulnerabilities will drive the adoption of secure markdown parsers and automated security testing in CI/CD pipelines, reducing the overall attack surface over time.
- +1: Cloud providers will enhance their native security features, offering built-in file validation and threat detection as standard services, making security more accessible to smaller teams.
- +1: Security researchers will develop more robust open-source tools for testing file upload vulnerabilities, democratizing the ability to find and fix these issues before attackers exploit them.
▶️ Related Video (78% Match):
🎯Let’s Practice For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by ThousandsIT/Security Reporter URL:
Reported By: Sans1986 Nevergivesup – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]
📢 Follow UndercodeTesting & Stay Tuned:
- Test Markdown Rendering Locally: Set up a local testing environment to verify sanitization:
- Disable Dangerous Features: Disallow `javascript:` URIs and `data:` URIs in markdown:
- Set Proper File Permissions: Ensure uploaded files are not executable:
- Perform Image Magic Validation: Use tools like `imagemagick` to verify the file is a genuine image:
- Verify MIME Type: Use server-side libraries to check the actual MIME type:
- Use Data URI for Direct Injection: In cases where direct HTML upload is blocked, try the data URI format:


