So, I found a website that uses AES encryption to prevent XSS and I'm wondering if this approach is inherently flawed.

It has an endpoint like execute.php?code=encryptedPayload which de-crypts the payload and directly executes it as JavaScript inside the page like ... <script>decryptedPayload</script> ....

The reasoning of the developer behind this is that only the page owner can craft valid encrypted Payloads, thus noone can forge malicious payloads.

I do have a valid Cipher-text and when I change a character in the middle of the Cipher-text, a block of 16 characters in the executed JavaScript turns to garbage. So I assume it uses 128bit AES-ECB?

So basically I'm able to alter the Cipher-text and get the de-crypted result back. The goal obviously is to craft a cipher-text that de-crypts to a valid malicious JavaScript payload.

I'm pretty sure the Key used is fixed. Not sure about the IV - so far I only got one. But even if the IV is random, the payload usually is also fixed.

This seems wrong to me. Is it?

Edit: assumed CBC before but I noticed only part in the middle changes, so ECB

  • Hint: CBC is malleable. Commented Nov 7, 2022 at 20:26
  • @JosephSible-ReinstateMonica I just realized it's probably just ECB
    – xsrf
    Commented Nov 7, 2022 at 20:45

2 Answers 2


Encryption isn't really suitable here, and ECB in particular isn't safe at all. What the site really wants is authentication with integrity protection. That's not something encryption inherently provides - it only provides confidentiality, which doesn't matter here at all (you can see what it decrypts to just by looking at the response!) - and while there exist "authenticated encryption" modes, ECB is very much not one.

The actual solution would be a digital signature or Message Authentication Code, which are ways to cryptographically prove that some data (the JS to reflect, in this case) was selected by the holder of a secret key, and not modified by anybody else. This is, for example, the principle behind JWTs (JSON Web Tokens), which are a common way for a server to store some data in a browser (usually in a cookie or JS local storage, but it can be in a URL) such that the data can't be tampered with and expires automatically.

Probable flaws in this system:

  1. Where does the encrypted text come from? If it's generated on the client using a key common across sessions, you're golden; that means the encryption key is on the client as well, and you can extract it and generate a valid XSS payload to use on other users.
  2. If it's generated on the server, does the client ever send to the server the string that it wants encrypted? If so, you can send that request yourself for any arbitrary payload, and receive it back as a valid ciphertext. This assumes that the server is doing the encryption (and decryption), and means you can't get the actual key, but that's OK; the server acts as an "encryption oracle" for you; you can ask it what the ciphertext for any string is, and it tells you. Of course, if the server uses a different key for each session, this won't work.
  3. If the key is unique per session, where is it stored? It might be in a cookie, or even in the URL at some point, such that you can potentially modify the key for another user. This is a more complicated attack, potentially requiring a network attack (man-in-the-middle), and there are mitigations (such as HSTS) that would make it non-viable, but in general, planting a cookie value of your choice is possible.
  4. If the ciphertext is generated on the server but not in a way you can control by sending your own request, you might still be able to craft a malicious payload by brute-forcing a few bytes at a time. JS is a pretty forgiving language, and will tolerate lots of things like bare strings that don't do anything. As such, you don't need to brute-force the desired values for an entire 16-byte block - this is totally infeasible - but can instead focus on just a few values at the start and end of each block, so long as the ones in the middle are delimited by stuff like /*...*/ or //...{newline} or "...";. This still isn't easy - brute-forcing four bytes at specific locations in a block requires trying four billion possibilities on average (256 tries per byte to the power of the number of bytes you want) - but that is potentially viable for a dedicated attacker (though perhaps not if you have to send a request to check every attempt by bouncing it off the server, depending on how many blocks you can try at once). You can potentially speed it up by checking every byte of every legitimate payload that the site generates for any useful "gadgets", such as a block that starts )//... (end a function call and comment the rest of the line), which you can then reuse in your payload. That's the great (terrible) thing about ECB: while you can't flip arbitrary bits within it, or re-order bytes even if you know what they are, you can re-order and re-use entire blocks to generate entirely new messages. Since you definitely have a decryption oracle (you pass arbitrary ciphertext to the execute.php endpoint and look at the response), you can in theory find blocks that will let you say anything you want given enough time, and JS makes it relatively easy to ignore the parts of the block you haven't brought under control.
  • Thank you! So yes, it's only option 4. The key is fixed, stored on the server and the server generates the payloads which do not contain user input (as far as I know so far). So while the approach is in fact inherently wrong, there is not an immediate or easy to abuse security risk. It still requires lots of brute-force to exploit. So while I can report this as issue, I probably won't be able to provide a PoC with an actual useful gadget. Do you agree?
    – xsrf
    Commented Nov 8, 2022 at 12:01
  • Probably yes, sorry. If you have enough ciphertext examples you can probably find enough interesting 16-byte blocks to string together some PoC, but it probably won't be arbitrary execution without a really untenable amount of work. With that said... this means the server has a pre-defined set of scripts it sends (since the client can't actually generate the ciphertext directly or indirectly), and that means there is an obvious correct way to achieve their goal securely: just specify the name of the script file, rather than doing this absurd encrypt-transmit-decrypt-reflect nonsense.
    – CBHacking
    Commented Nov 9, 2022 at 7:23

Encryption cannot protect from XSS. User A provides some text that is stored then on the server. User B loads this text from server and uses it to render the page. If the text used for rendering mutates DOM directly and if the text contains malicious code, we have XSS.

It does not matter, if the text provided by user A was encrypted before sending to the server. If does not matter, if the text loaded by user B was encrypted, and will be decrypted in the browser just before execution. Important is only the fact that the malicious code provided by User A has reached user B.

How to prevent XSS?

  • On server: Filter any data sent by client, remove any code that can be used for XSS, e.g. pieces of JavaScript.
  • On client: Escape any dynamic data before modifying DOM.
  • See further OWASP hints.
  • you didn't understand. I'm not talking about transport encryption and there is nothing stored on the server - my case is reflected XSS. I'm also not asking for how to prevent XSS. I know how to prevent XSS.
    – xsrf
    Commented Nov 7, 2022 at 21:39
  • @xsrf: Then please explain it better. You wrote "de-crypts the payload and directly executes it as JavaScript inside the page" - After decryption it does not matter if the code was encrypted before or not. How does it differ from the usual XSS?
    – mentallurg
    Commented Nov 8, 2022 at 2:40
  • it differs because you can't just forge an encrypted payload, you don't know the encryption key. at least that's the reasoning of the website developer
    – xsrf
    Commented Nov 8, 2022 at 5:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .