top of page
  • Writer's pictureAdi "Adico" Cohen

XSS in Gmail's Amp4Email


AMP is most commonly used as a framework to develop fast-loading content on the web.

One of AMP's projects, AMP4Email has been adopted in recent years by many of the leading mail services as a way to provide Dynamic Emails (essentially a subset of regular HTML with a few default components to handle things like layouts, templates, forms, and such).

When I first heard about this feature a few years ago, my initial reaction (like many of you I’m sure) was “This can’t be secured! How did they prevent XSS?” and Boy was I wrong!

Gmail has a great setup where you could easily write and validate your AMP Emails through their Playground website. And even send it to your mailbox to see how it renders in Gmail, Perfect for security research :)

After spending a few days playing around in that sandbox I had about 5 different vectors that resulted in either broken HTML (that could potentially be exploited with some extra work) or just complete XSS vectors. Sounds pretty easy right? Or so I thought.

When I attempted to send any of those vectors to Gmail, I quickly discovered there’s either a second filter in play, or a completely different version of AMP with additional security validations.

In this post, I’ll walk through how I managed to get one of those initial vectors to bypass that extra layer and arrive at my inbox.


I found out over the years that the easiest way to circumvent an XSS filter is by tricking it into a different rendering context than what the browser will actually use to render a given piece of code.

For this to work, I need to be able to write a payload that contains more than one rendering context. Plain HTML is given as the first context. But additional ones can be achieved through many means (Templates, SVG, Math, CSS, etc…)

In the scope of AMP4Email however, most of those are forbidden and one of my only real options is Stylesheets, so I’ve decided to focus my research around that.

Initial Vector

For my attack to work, I needed to find a discrepancy between how the filter renders a stylesheet and how the browser does.

This means either tricking the filter into believing a fake style tag (either opening or closing) is real and should be treated as such, when in reality the browser will ignore it. Or the exact opposite, treat a real tag as being fake and ignore it.

As I mentioned above, I already had a vector that successfully triggered an XSS in the AMP playground but was not able to bypass Gmail’s filter.

Here it is:

<style amp-custom>body{color:red}</styleX>
<meta name="</style><img src='x'onerror=alert(1)">

The reason this vector works is that AMP is being slightly too greedy, and leaves the CSS context as soon as it encounters the string “</style” even if it doesn’t have a closing bracket “>” or at least a whitespace after it.

I assume this was in place to mitigate other attacks. But I was able to use this to trick the filter into believing we’re back in HTML context, while the browser obviously ignores </styleX> entirely and stays well within the realm of CSS.

Next, I take advantage of this rendering context mismatch to position the XSS in a way that looks safe to the filter. I chose the name attribute of a <meta> tag but any safe attribute would work here.

But when the browser (which still renders CSS at this point) encounters this tag, it counts it as malformed CSS, terminates the stylesheet at the real </style> tag and renders the <img> tag with its onerror attribute, triggering the XSS.

If you try sending this payload to Gmail however, the entire document fails to load. (which is what happens when the filter encounters a significant validation error)

Some (More) Research

At this point, I thought I could still sneak this payload by Gmail and to start off, I tried sending a few “harmless” payloads to get a sense of what restrictions are in place before I try anything too malicious.

This included tests such as:

<style>div{font-family:'aa/*f<br> ff*/'}</style>
<style>div{font-family:'aa/*f</xxx> ff*/'}</style>
<style>div{font-family:'aa/*f</style> ff*/'}</style>

I quickly found out that while AMP allows any value to be present inside this string-comment hybrid, Gmail does not.

Everything worked well with the first two payloads, they arrived at my inbox with only a minor change, they were escaped.

<style>div{font-family:'aa/*f</xxx> ff*/'}</style> 

Transformed into

<style>div{font-family:'aa/*f\00003c/xxx\00003e ff*/'}</style>

And since I can't terminate a tag without HTML entities(‘<’ , ‘>’) What looked like a promising vector in AMP, seemed way less interesting after Gmail ran its magic on it.

I spent some more time trying to put HTML entities in different locations of the CSS statement until I reached the holy grail, which is non-other than my beloved CSS Selector!

If we send the following payload to Gmail

<style amp-custom>[id='a<br>aa'],body{font-family:'aaaa'}</style>  

we get back the exact same thing, no escaping or other mutations take place

Great, I thought, so all I need now is to copy in my </styleX><meta…> payload and be done, right?

I wrote it down in the AMP playground editor only to immediately receive an error.

Damn, so I can’t “fake” closing the tag in the middle of a selector because AMP detects it as malformed CSS.

At this point, I still thought I could jiggle the payload a little and make it work so I tried to find the cutoff of where AMP errors out.

And just as expected, it happens after encountering “</style

I decided to delete the “e” and send “</styl>” to see how Gmail handles this case.

And this might not surprise you at this point, but Gmail did not appreciate my attempt

So now I know that Gmail has an even stricter filter and just deletes the payload as soon as anything resembling a closing </style> tag is encountered. Great. Thanks Google.

The Payload

Before I completely gave up on this direction, I had one last idea left I wanted to try.

Since every other CSS Context encoded my HTML Entities besides Selectors, what would happen if I send an Encoded Selector to Gmail? Will it get decoded for me?

I started out again with the safest payload I could come up with, just to ensure that If it does get filtered it will be because of the encoding and nothing else.

So I sent the following snippet:

<style amp-custom>[id='a<b\000072> aa'],body{font-family:'aaaa'}</style>

And Yes! It worked! Gmail actually decoded the \000072 into the letter ‘r

Now for the real test. Can I use this to inject a closing style tag?

<style amp-custom>[id='a</st\000079le> aa'],body{font-family:'aaaa'}</style>

Comes out as:

I sure can!

At this point I could completely ditch the <meta> tag, it’s not needed because AMP rightfully doesn’t treat </st\000079le> as anything it should worry about. And this only mutates to a usable payload after Gmail’s extra filter is executed.

To tie everything together, I came up with the following final payload that injects an <img> tag, but at this point, any HTML could be used:

  <style amp-custom>[id='</st\000079le></head><body>
  <img src= onerror=a=1>']{color:blue}</style>

I could immediately see this worked when I opened the email and noticed the broken image.

Looking closer at the dev-tools confirmed it

A new XSS was born

Only to then quickly died because I reported this to Google :)


  • Mar 27th 2021 - Reported issue to Google

  • Apr 1st 2021 - “Nice Catch” response from Google

  • Apr 13th 2021 - Awarded a bounty of 5000$

  • Jul 7th 2022 - Notice the issue was fixed (in reality this was fixed way quicker, there was just an issue with the notice)

  • Jul 9th 2022 - Blog post published

7,284 views0 comments

Recent Posts

See All

JSON-based XSS exploitation

The following post describes a new method to exploit injections in JSON file - Back in 2012 Introduction: In the world of Web2.0 and mash web applications, security researchers come across more and mo

Microsoft Anti-XSS Library Bypass (MS12-007)

The following post describes the second bypass I found to the toStaticHTML function in IE - back in 2012. Introduction: The Microsoft Anti-XSS Library is used in ASP .NET applications to protect again


bottom of page