<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ben Regenspan</title>
    <description>The latest articles on Forem by Ben Regenspan (@benregenspan).</description>
    <link>https://forem.com/benregenspan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F267473%2F7ea52345-2adc-4b5e-9234-8d73f2caedc8.jpeg</url>
      <title>Forem: Ben Regenspan</title>
      <link>https://forem.com/benregenspan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/benregenspan"/>
    <language>en</language>
    <item>
      <title>The state of JSONP (and JSONP vulnerabilities) in 2021</title>
      <dc:creator>Ben Regenspan</dc:creator>
      <pubDate>Sun, 07 Feb 2021 01:53:10 +0000</pubDate>
      <link>https://forem.com/benregenspan/the-state-of-jsonp-and-jsonp-vulnerabilities-in-2021-52ep</link>
      <guid>https://forem.com/benregenspan/the-state-of-jsonp-and-jsonp-vulnerabilities-in-2021-52ep</guid>
      <description>&lt;h2&gt;
  
  
  What is JSONP?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/Security/wiki/Same_Origin_Policy" rel="noopener noreferrer"&gt;Same-Origin Policy&lt;/a&gt; is a foundational web security feature. It ensures that an attacker with control of &lt;strong&gt;Site A&lt;/strong&gt; cannot trivially gain access to data from &lt;strong&gt;Site B&lt;/strong&gt;. Without the Same-Origin Policy, JavaScript running on example.com could simply &lt;code&gt;fetch('https://www.facebook.com')&lt;/code&gt;, read your private information, and do what it wants with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmt7ee2sx5rsm9oi3lj4i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fmt7ee2sx5rsm9oi3lj4i.png" width="686" height="210"&gt;&lt;/a&gt;&lt;/p&gt;
The Same-Origin Policy in action: screenshot of Chrome Developer Tools highlighting that an attempt to call "fetch('https://facebook.com')" while on a non-Facebook Origin is automatically blocked by the browser.



&lt;p&gt;But what happens when the same company owns both &lt;strong&gt;Site A&lt;/strong&gt; and &lt;strong&gt;Site B&lt;/strong&gt; and wants to share data between them? Or when the owner of &lt;strong&gt;Site B&lt;/strong&gt; wants to expose an API that &lt;strong&gt;Site A&lt;/strong&gt; can access through client-side JavaScript?&lt;/p&gt;

&lt;p&gt;These days, the answer is clear: sites can (and should) use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS standard&lt;/a&gt;. With CORS, &lt;strong&gt;Site B&lt;/strong&gt; can explicitly permit &lt;strong&gt;Site A&lt;/strong&gt; to make certain requests.&lt;/p&gt;

&lt;p&gt;But before CORS, there were hacks, and the most prominent one was &lt;a href="https://en.wikipedia.org/wiki/JSONP" rel="noopener noreferrer"&gt;JSONP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;JSONP takes advantage of the fact that the same-origin policy does not prevent execution of external &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. Usually, a &lt;code&gt;&amp;lt;script src="some/js/file.js"&amp;gt;&lt;/code&gt; tag represents a static script file. But you can just as well create a dynamic API endpoint, say &lt;code&gt;/userdata.jsonp&lt;/code&gt;, and have it behave as a script by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepting a query parameter (such as &lt;code&gt;?callback=CALLBACK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Returning a &lt;code&gt;Content-Type: application/javascript&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;Having your server return a Javascript response that invokes the passed-in callback function name and passes it some data retrieved from the active user's session:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;CALLBACK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ben&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
Example server response. The user data is retrieved serverside based on the user's session cookie and padded with a JavaScript function call.





&lt;p&gt;Now &lt;strong&gt;Site A&lt;/strong&gt; can add a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
   &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CALLBACK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callbackFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://api.example.com/userdata.jsonp?callback=CALLBACK"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and JavaScript running on &lt;strong&gt;Site A&lt;/strong&gt; has access to the user data returned from &lt;strong&gt;Site B&lt;/strong&gt; (api.example.com).&lt;/p&gt;

&lt;h2&gt;
  
  
  (Some of) the problems with JSONP
&lt;/h2&gt;

&lt;p&gt;In the example above, &lt;strong&gt;Site B&lt;/strong&gt; is intentionally exposing unrestricted access to the logged-in user's details. Probably a bad idea! That's why sites that implement similar APIs via JSONP will typically check the &lt;code&gt;Referer&lt;/code&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; header to see if the referring hostname is allowed, and only return session-specific data if so.&lt;/p&gt;

&lt;p&gt;Unfortunately, checking the &lt;code&gt;Referer&lt;/code&gt; header is imperfect, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are various cases where browsers omit &lt;code&gt;Referer&lt;/code&gt; headers. Also some users may have browser extensions that remove them for privacy-protection reasons, and modern browsers expose ways for requestor sites to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy" rel="noopener noreferrer"&gt;intentionally strip &lt;code&gt;Referer&lt;/code&gt;&lt;/a&gt; from requests.

&lt;ul&gt;
&lt;li&gt;To account for this, developers sometimes (incorrectly) treat the case where no referrer is present the same as the case where a valid referrer is present.&lt;/li&gt;
&lt;li&gt;(The &lt;code&gt;Origin&lt;/code&gt; header can be used instead, but most JSONP endpoints were created to support older browsers, many of which didn't yet send the &lt;code&gt;Origin&lt;/code&gt; header.)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;In the past there were ways to fake &lt;code&gt;Referer&lt;/code&gt; headers (e.g. &lt;a href="https://hackerone.com/reports/10373" rel="noopener noreferrer"&gt;through Flash&lt;/a&gt;)&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This problem has left a lot of sites vulnerable over the years.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSONP in the wild
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3qphnbznknpcg8halms8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3qphnbznknpcg8halms8.png" alt="Google Trends chart showing interest in " width="541" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the chart above we can see that interest in "JSONP" as measured by Google searches peaked in 2012 and has declined to nearly nothing since. So we know it doesn't appear to be a popular technology to use going forwards, but how much usage is still lingering on the web?&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://httparchive.org/" rel="noopener noreferrer"&gt;HTTP Archive&lt;/a&gt; regularly crawls top sites on the web and stores various technical details. Crawl results can be queried via &lt;a href="https://github.com/HTTPArchive/httparchive.org/blob/master/docs/gettingstarted_bigquery.md" rel="noopener noreferrer"&gt;Google BigQuery&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Earlier, we saw that JSONP endpoints typically accept a &lt;code&gt;?callback=&lt;/code&gt; GET parameter and return a &lt;code&gt;Content-Type: application/javascript&lt;/code&gt; header. This gives us a heuristic to use to search through an HTTP Archive crawl and identify sites that still use JSONP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;REGEXP_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'([^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;[^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;]+)$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;req_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="nv"&gt;`httparchive.summary_requests.2021_01_01_desktop`&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'script'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;REGEXP_CONTAINS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'callback='&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;req_domain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query lists domains that appear to expose and actively use JSONP endpoints, as well as one example JSONP endpoint URL for each. This particular crawl found 12,409 unique domains with apparent JSONP endpoints (which is 0.65% of the total number of unique domains in the crawl):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fke0niczuwb3rod1pneji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fke0niczuwb3rod1pneji.png" width="800" height="696"&gt;&lt;/a&gt;&lt;/p&gt;
Screenshot showing results of running the earlier-noted query in Google BigQuery. This is for illustrative purposes; pictured results do not contain obvious vulnerabilities, just show JSONP in active use.



&lt;p&gt;This goes to show that even though JSONP is an outdated technique, it still has fairly significant use in the wild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying for vulnerabilities
&lt;/h2&gt;

&lt;p&gt;The vast majority of the endpoints we found above are unlikely to contain vulnerable use of JSONP. Many are cases where JSONP is used to deliver relatively low-risk features like third-party widgets (e.g. a feed of recent Instagram posts) or analytics requests which don't modify or return user data.&lt;/p&gt;

&lt;p&gt;But it's possible to refine the query down further. Through another version of the query, I found a suspicious JSONP endpoint on a major site. Then I verified that it was exploitable in the case where no &lt;code&gt;Referer&lt;/code&gt; header is sent&lt;sup id="fnref2"&gt;2&lt;/sup&gt;, and that it can leak user session data (I reported the issue and am leaving out identifying information here).&lt;/p&gt;

&lt;p&gt;In the case where I did find this vulnerability, only a single modern browser (Firefox) was vulnerable. Read on for why...&lt;/p&gt;

&lt;h2&gt;
  
  
  Recent web platform improvement: &lt;code&gt;SameSite&lt;/code&gt; cookies
&lt;/h2&gt;

&lt;p&gt;The JSONP endpoint in our example relies on session cookies to authenticate the user. Even though &lt;strong&gt;Site A&lt;/strong&gt; can't read cookies from &lt;strong&gt;Site B&lt;/strong&gt;, it can still request certain resources (such as the JSONP endpoint) from it. And until recently, browsers would generally send cookies along with these third-party requests. This allows the JSONP endpoint on &lt;strong&gt;Site B&lt;/strong&gt; to return the same authenticated state that it would return to a user who visited &lt;strong&gt;Site B&lt;/strong&gt; directly, without which the endpoint simply wouldn't function.&lt;/p&gt;

&lt;p&gt;There were a number of issues with this default behavior, and JSONP CSRF is only one of the vulnerabilities it enabled (even leaving aside privacy issues). So a &lt;code&gt;SameSite: (Lax|Strict|None)&lt;/code&gt; cookie attribute was introduced which controls whether specified cookies are sent in cross-site requests. And starting in 2020, &lt;a href="https://www.chromestatus.com/feature/5088147346030592" rel="noopener noreferrer"&gt;browsers began setting this attribute to a safe default&lt;/a&gt;. This is likely to eliminate many active vulnerabilities, because site authors now must explicitly opt in to dangerous behavior by marking cookies as &lt;code&gt;SameSite: None&lt;/code&gt;. Many of the JSONP endpoints in the wild may be forgotten by their authors and will break silently, fixing the vulnerabilities (this is likely what happened in the case of the Firefox-only issue I found via the HTTP Archive); other breakages might be noticed and serve to encourage a switch to safer techniques.&lt;/p&gt;

&lt;p&gt;MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility" rel="noopener noreferrer"&gt;browser compatibility table here&lt;/a&gt; shows how most modern browsers have moved to this new secure-by-default behavior:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fd9226iorcb4kjptrfzqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fd9226iorcb4kjptrfzqj.png" alt="Extract of Mozilla Developer Network's table of browser compatibility support for SameSite=Lax and the behavior of defaulting to SameSite=Lax. All modern desktop browsers besides Firefox now are marked as defaulting to SameSite=Lax" width="602" height="373"&gt;&lt;/a&gt;&lt;/p&gt;
Extract of SameSite=Lax browser support table from developer.mozilla.org as of 2021/01/23



&lt;p&gt;Safari is marked as missing this improvement in the table above, but it fixed the underlying issue &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=198181#c44" rel="noopener noreferrer"&gt;by other means&lt;/a&gt; (simply blocking all third-party cookies), in mid-2020.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For developers:&lt;/strong&gt; One big lesson is to avoid using JSONP. It's very likely that you no longer need it given that browsers as far back as IE10 had CORS support, and even large enterprises and my in-laws have long given up on IE9 (a browser released 10 years ago) at this point. (I'm not saying all sites already using JSONP should rewrite, most have low-risk use cases that involve delivering a user-agnostic response that can't result in unexpected information disclosure.)&lt;/p&gt;

&lt;p&gt;Another lesson is to just generally be cautious about using techniques that work around web standards and the browser's default security model, but sometimes this is easier said than done. JSONP served a very useful purpose, and its ubiquity helped push the web platform to improve, encouraging browsers to build in safer options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For security researchers:&lt;/strong&gt; HTTP Archive data could be worth playing around with in BigQuery. There are a lot of possible searches that I left unexplored in this post, including a search for sites that have JSONP endpoints &lt;em&gt;and&lt;/em&gt; deliberately mark some cookies as &lt;code&gt;SameSite=None&lt;/code&gt; (meaning that any exploit found would more likely be exploitable cross-browser).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the Firefox team:&lt;/strong&gt; Following other browsers down the path of defaulting to &lt;code&gt;SameSite=Lax&lt;/code&gt;(or following Safari in blocking all third-party cookies) sooner rather than later  would help to patch up some active vulnerabilities on the web.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Yes I'm spelling it correctly - the "referrer" header name is misspelled as &lt;code&gt;referer&lt;/code&gt; per the &lt;a href="https://tools.ietf.org/html/rfc7231#section-5.5.2" rel="noopener noreferrer"&gt;specification&lt;/a&gt;. Thank you to whoever wrote it out that way originally, as this decision has probably saved a lot of electricity over the years. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;In the past, you would need to be slightly more clever to ensure a &lt;code&gt;Referer&lt;/code&gt; isn't sent, but thanks to a privacy feature that modern browsers have adopted, it's as simple as adding a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/referrerPolicy" rel="noopener noreferrer"&gt;&lt;code&gt;referrerPolicy="no-referrer"&lt;/code&gt;&lt;/a&gt; attribute to the JSONP script tag. This is an interesting illustration of unintended consequences from security and privacy improvements -- there was a time when browsers more reliably sent &lt;code&gt;Referer&lt;/code&gt; headers, and developers thought they could generally assume their presence; various privacy-oriented improvements have ensured that this is no longer the case. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>security</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
