September 20, 2026
CSP Recon: Reading a Policy Like a Hunter
A WAF block means your request never arrived. A CSP block means your injection worked perfectly and the browser refused to run it.

By Nitin yadav
7 min read
Hello, I am Nitin.
Let me tell you about the worst four hours I ever spent on a bug bounty program.
I had found a reflected injection point on a large fintech target. Beautiful reflection, straight into the HTML body, angle brackets surviving untouched. I fired the payload, and the page loaded with nothing happening. No alert. No error visible.
So I did what everyone does. I tried a different payload. Then a different tag. Then an SVG. Then event handlers nobody uses. Then encoded variants. Then I went and found a WAF bypass list and started working through it like a man checking every door in a corridor.
Four hours. Nothing.
Then I opened the developer console properly and actually read it, instead of glancing at it. Sitting there, repeated for every attempt I had made, was a line saying the browser had refused to execute an inline script because it violated the page's Content Security Policy.
The injection had worked every single time. All four hours of it. The markup was in the DOM. The browser was simply declining to run it, and telling me so, in plain language, the entire time.
I had been trying to pick a lock on a door that was already open, on a room with a second door I had never looked at.
That day taught me the lesson this post is about: read the policy before you write the payload. Not after. Before. Because the policy does not just tell you whether you will be blocked. It tells you, quite specifically, which kind of bug you should be looking for on that target.
What CSP actually is, in one minute
Content Security Policy is a header the server sends that tells the browser what the page is allowed to do. Which scripts may run. Which hosts resources may come from. Whether inline code is permitted at all.
The critical thing to understand is where it sits. A WAF lives in front of the application and inspects requests. CSP lives in the browser and governs execution. They fail in opposite directions, which is why day nineteen and today are completely different problems.
A WAF blocking you means your request never arrived. CSP blocking you means your injection succeeded perfectly and the browser refused to run the result.
Those two situations look identical if you only watch the page. They look nothing alike if you watch the console. Which brings me to the single most useful habit in this entire post.
Habit one: keep the console open, always
Every CSP violation prints a message. The browser tells you, unprompted and in readable English, exactly which directive it enforced and what it stopped.
That message is not noise. It is the target's security team describing their own configuration to you, for free, every time you touch the page.
If I had read it on hour one instead of hour four, my day would have gone: injection confirmed, inline execution blocked by policy, therefore stop writing inline payloads and start looking for a way to load an external script or find a gadget. That is a completely different afternoon.
Watch the console. Read the violation. Let it redirect you.
Step one: collect every policy on the page
More than one can apply, and they are not alternatives โ they intersect. When two policies are present, content must satisfy both. That means an application adding a second policy is adding restriction, not replacing the first.
Get the header:
curl -sI <https://target.com/page> | grep -i "content-security-policy"curl -sI <https://target.com/page> | grep -i "content-security-policy"Note that there are two header names. One enforces. The other reports only, and enforces nothing at all. If a target ships only the report-only variant, they have visibility and no protection, and that is worth knowing immediately โ it means CSP is not standing in your way despite appearing to.
Then check the page body, because a policy can also be delivered in a meta tag:
curl -s <https://target.com/page> | grep -i "http-equiv=\"Content-Security-Policy\""curl -s <https://target.com/page> | grep -i "http-equiv=\"Content-Security-Policy\""And check several different pages. Policies commonly vary across an application โ the marketing pages, the authenticated app, the admin area, and the file-preview endpoint are frequently configured by different people at different times. The weakest policy on the domain is the one you want to work against, provided you can find an injection there.
Step two: read script-src, the main gate
This directive decides whether your injected code runs. Four things to look for, in order of how happy each should make you.
Permission for inline code. If the policy permits inline script execution, CSP is not stopping ordinary XSS at all. Your payload runs. This is far more common than it should be, because removing inline script from a legacy application is genuinely hard work and teams ship the permissive version intending to tighten later.
Permission for string evaluation. If code evaluation from strings is allowed, that is a second route even when inline is blocked, and it makes many gadgets viable.
A host allow-list. A list of domains scripts may load from. This looks strict and usually is not, for reasons that are the whole of tomorrow. Every entry on that list is a place you might be able to get a script from.
A nonce or a hash. The strong configuration. Only scripts carrying the right nonce value, or matching a listed hash, may execute. If you see this, ordinary inline injection is dead and you need a different approach.
Step three: the gap almost everyone leaves open
Here is the detail that pays more than anything else on this page, and it comes from a genuine trap in how CSP is designed.
Most directives fall back to a default. Set a default and forget to set a specific one, and the default covers it. That is the intuition every developer has, and it is correct for most directives.
Two important directives have no fallback at all.
The directive controlling where the document's base URL may be set, and the directive controlling where forms may submit, are not covered by the default. If they are not written explicitly, they are unrestricted โ no matter how strict everything else looks.
This is the single most common CSP mistake in production. A team writes a careful policy with a nonce, locks down script sources, feels good about it, and leaves the base URL unconstrained because they reasonably assumed the default covered everything.
Why that matters: controlling a page's base URL changes where every relative URL on that page resolves to. Including relative script sources. The policy still says scripts may only come from approved hosts, and you have just changed what the browser thinks the approved path points at.
So when you read a policy, do this immediately:
Search for the base-uri directive. If it is absent, write that down in capital letters. Tomorrow's post is largely about what you do with it.
Same check for form-action. Its absence means a form you inject can submit anywhere, which is a credential-capture path that needs no script execution whatsoever.
Step four: the directives people forget
Object and embed sources. If unrestricted, elements that load plugin content can carry data URLs containing a whole document. Many policies lock scripts tightly and never think about this one.
Frame ancestors. Not about XSS directly, but it tells you whether the page can be framed โ which decides whether day ten's cross-window message attacks have a delivery route.
Trusted Types enforcement. From day fourteen. If present, DOM sinks reject plain strings and a large part of week two is closed on that page. Knowing this early saves hours.
Connect sources. Where the page may send data. Relevant when you are thinking about exfiltration paths in a report.
Step five: reach a verdict, then pick your bug
This is the payoff, and it is why I said the policy tells you which bug to hunt.
Inline permitted โ CSP is not your problem. Hunt XSS normally, exactly as in weeks one and two.
Host allow-list, no nonce โ do not waste time on inline payloads. Your route is getting a script from a host already on that list. Tomorrow.
Nonce or hash, base-uri absent โ the base URL gap is your primary target. Tomorrow.
Nonce or hash, base-uri present, everything tight โ inline injection will not execute. Pivot to finding a gadget in already-trusted code, or accept that this page is hardened and spend your time on a different page with a weaker policy. That is not defeat; that is correct triage.
Report-only โ nothing is enforced. Proceed as though there is no policy, and mention in your report that their policy is not actually protecting anything, because they may not realise.
A finding hiding in plain sight
One more thing worth knowing. The policy itself can be the bug.
If any part of the policy is built from user-controlled input โ a parameter that ends up in a report URI, a value reflected into the header โ you may be able to inject directives or break the policy entirely. Test whether anything you control appears in that header.
And a subtler one: a policy that permits a host you can upload content to is a policy that permits scripts from you. Check whether the allow-list includes any storage bucket, CDN path, or user-content domain associated with the same product. That combination has ended more than one nominally-strict policy.
Impact ladder
- Informational โ strict policy, no gaps. Record it. Re-read after their next release, because policies drift.
- Low โ report-only policy mistaken for enforcement, or missing directives with no demonstrated path.
- Medium โ missing base-uri or form-action with a plausible injection point, reported as a hardening gap.
- High โ a demonstrated bypass, which is tomorrow's material. Severity is the underlying XSS.
- Critical โ a permissive policy plus stored injection on a shared or staff-facing surface.
Frame the recon findings honestly. A missing directive with no injection point to pair it with is a hardening recommendation, not a vulnerability, and reporting it as though it were will cost you credibility. Say what it is: a gap that removes a layer of defence, which becomes serious the moment any injection exists on that page.
Conclusion โ steal this checklist
- Read the policy before writing the payload. It tells you which bug to hunt on that target.
- Keep the console open. Every violation message is the target describing its own configuration to you for free.
- CSP blocking you means your injection worked and execution was refused. That is the opposite of a WAF block and needs the opposite response.
- Collect both the header and the meta tag, and remember multiple policies intersect rather than replace.
- Check whether the policy is enforcing or report-only. Report-only enforces nothing.
- Compare policies across pages. The weakest one on the domain is where you want an injection.
- In script-src, check in order: inline permitted, string evaluation permitted, host allow-list, nonce or hash.
- The base URL and form submission directives have no fallback from the default. Absent means unrestricted. This is the most common mistake in production.
- Check object sources, frame ancestors, Trusted Types enforcement, and connect sources too.
- Reach a verdict and let it direct you: inline permitted means hunt normally; allow-list means find a trusted-host route; nonce with a base-uri gap means go for the base tag; fully strict means find a gadget or move to another page.
- Check whether any part of the policy is built from input you control.
- Check whether the allow-list includes a host you can upload content to.
Tomorrow: the bypass playbook โ turning each of these gaps into execution.
If you Love reading my blogs. Check my Youtube Channel too.