Friday, February 15, 2013

Are you sure you use JSONP properly?

This is a friendly reminder about old problem with JSONP.

We need JSONP to receive some data on domain1 from domain2 because of cross origin restrictions. We put <script src="domain2?callback=CALLBACK"></script> where CALLBACK is name of Javascript function which handles received data. Here comes the rule: JSONP Should Never Be Used For Personal Data.(unless you have a special token, different for every user).

Knowledge "Who am I" is a secret. When I visit new website it's not supposed to know who I am, my email, name or sometimes my private messages. With JSONP a common vector can be:
<script src=data_provider/me.json?callback=leak></script>
By inserting this script any website can automatically receive information about me from data_provider.

Some showcases:
Stripe(fixed): https://manage.stripe.com/ajax/api/customers?count=20&callback=leak
Shopify(fixed): https://SITE.myshopify.com/admin/payments.json?callback=leak
Disqus(api_key is same for all users): http://disqus.com/api/3.0/users/listActivity.json?include=user&limit=10&api_key=Y1S1wGIzdc63qnZ5rhHfjqEABGA4ZTDncauWFFWWTUBqkmLjdxloTb7ilhGnZ7z1&callback=leak

Disqus refused to fix it explaining that "exposed data is not private", lol, but it's still personal.

Why this happens? Sometimes JSONP for private data applied by purpose - this means developers are bad at web security. But more interesting case when, for example, team adds Rack::JSONP middleware for Feature1 but it is designed to add callback in all JSON responses. You should know your stack well. 
API must not detect user by his cookies (about "broken" cookies' nature). If you "proxy" your API through website make sure to add additional CSRF token in the headers. Yes, for GET too. Because CSRF token's main goal is to protect your cookies. This is not about POST or GET, this is about your cookies == credentials.

By the way people often forget to filter and sanitize "callback" parameter. It can lead to XSS in IE6-8, because it detects content type from file extension:
URL/api.html?callback=<script>...</script>

It's also a handy Content Security Policy (denying 3rd party script sources) bypass - <script src="local.json?callback=payload//">

I recommend to allow [a-zA-Z0-9] callbacks only and use HTML5 cross domain sharing techniques rather than this insecure trick. Thanks for reading, share your ideas

10 comments:

  1. Alternatively, this can be done:
    http://stackoverflow.com/questions/2669690/why-does-google-prepend-while1-to-their-json-responses

    ReplyDelete
    Replies
    1. this is unrelated. JSON != JSONP

      Delete
    2. It's kind of related actually, though it wouldn't work with JSONP.

      Delete
    3. JSON hijacking is vuln in browsers, JSONP hijacking is vuln in web apps

      Delete
  2. Do you have any plans to write about how you became interested in web security and what resources you use to educate yourself? Now that would be fun to read.

    ReplyDelete
  3. Hi Egor, can you explain why sanitizing the callback parameter would make sense? A malicious website that controls the callback parameter already has all the control over the result it might want to have. How can it get any worse than that? How could the IE problem you suggest be worse than letting a website executing arbitrary javascript?

    ReplyDelete
    Replies
    1. there is Content Security Policy - it denies inline script execution. Thus you need a "reflector" on same domain. You can put XSS in callback and insert it in HTML. CSP will not block it

      Delete
  4. Please don't only allow [a-zA-Z0-9] callbacks, this will block valid callbacks like "Foo.bar" as well as valid characters like underscores. Your JavaScript should *always* be namespaced so it's most likely going to have a '.' in it.

    ReplyDelete
    Replies
    1. Definitely. We should just blog HTML chars

      Delete