What is JSONP?
The Same-Origin Policy is a foundational web security feature. It ensures that an attacker with control of Site A cannot trivially gain access to data from Site B. Without the Same-Origin Policy, JavaScript running on example.com could simply fetch('https://www.facebook.com')
, read your private information, and do what it wants with it.
But what happens when the same company owns both Site A and Site B and wants to share data between them? Or when the owner of Site B wants to expose an API that Site A can access through client-side JavaScript?
These days, the answer is clear: sites can (and should) use the CORS standard. With CORS, Site B can explicitly permit Site A to make certain requests.
But before CORS, there were hacks, and the most prominent one was JSONP.
JSONP takes advantage of the fact that the same-origin policy does not prevent execution of external <script>
tags. Usually, a <script src="some/js/file.js">
tag represents a static script file. But you can just as well create a dynamic API endpoint, say /userdata.jsonp
, and have it behave as a script by:
- Accepting a query parameter (such as
?callback=CALLBACK
) - Returning a
Content-Type: application/javascript
header - 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:
Now Site A can add a few lines:
<script>
window.CALLBACK = function callbackFunction(userData) {
console.log(userData.payload.username);
}
</script>
<script src="http://api.example.com/userdata.jsonp?callback=CALLBACK"></script>
...and JavaScript running on Site A has access to the user data returned from Site B (api.example.com).
(Some of) the problems with JSONP
In the example above, Site B 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 Referer
1 header to see if the referring hostname is allowed, and only return session-specific data if so.
Unfortunately, checking the Referer
header is imperfect, because:
- There are various cases where browsers omit
Referer
headers. Also some users may have browser extensions that remove them for privacy-protection reasons, and modern browsers expose ways for requestor sites to intentionally stripReferer
from requests.- 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.
- (The
Origin
header can be used instead, but most JSONP endpoints were created to support older browsers, many of which didn't yet send theOrigin
header.)
- In the past there were ways to fake
Referer
headers (e.g. through Flash)
This problem has left a lot of sites vulnerable over the years. There are various names in use to describe this vulnerability, but we can call the specific attack a "JSONP-based CSRF", and the outcome of the attack is Information Disclosure, which can sometimes enable further attacks, such as Session Hijacking.
JSONP in the wild
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?
The HTTP Archive regularly crawls top sites on the web and stores various technical details. Crawl results can be queried via Google BigQuery.
Earlier, we saw that JSONP endpoints typically accept a ?callback=
GET parameter and return a Content-Type: application/javascript
header. This gives us a heuristic to use to search through an HTTP Archive crawl and identify sites that still use JSONP:
SELECT
REGEXP_EXTRACT(req_host, r'([^\.]+\.[^\.]+)$') as req_domain,
MAX(url) as url,
FROM
`httparchive.summary_requests.2021_01_01_desktop`requests
WHERE
type = 'script'
AND
REGEXP_CONTAINS(url, 'callback=')
GROUP BY req_domain
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):
This goes to show that even though JSONP is an outdated technique, it still has fairly significant use in the wild.
Querying for vulnerabilities
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.
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 Referer
header is sent2, and that it can leak user session data (I reported the issue and am leaving out identifying information here).
In the case where I did find this vulnerability, only a single modern browser (Firefox) was vulnerable. Read on for why...
Recent web platform improvement: SameSite
cookies
The JSONP endpoint in our example relies on session cookies to authenticate the user. Even though Site A can't read cookies from Site B, 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 Site B to return the same authenticated state that it would return to a user who visited Site B directly, without which the endpoint simply wouldn't function.
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 SameSite: (Lax|Strict|None)
cookie attribute was introduced which controls whether specified cookies are sent in cross-site requests. And starting in 2020, browsers began setting this attribute to a safe default. This is likely to eliminate many active vulnerabilities, because site authors now must explicitly opt in to dangerous behavior by marking cookies as SameSite: None
. 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.
Safari is marked as missing this improvement in the table above, but it fixed the underlying issue by other means (simply blocking all third-party cookies), in mid-2020.
Lessons
For developers: 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.)
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.
For security researchers: 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 and deliberately mark some cookies as SameSite=None
(meaning that any exploit found would more likely be exploitable cross-browser).
For the Firefox team: Following other browsers down the path of defaulting to SameSite=Lax
(or following Safari in blocking all third-party cookies) sooner rather than later would help to patch up some active vulnerabilities on the web.
-
Yes I'm spelling it correctly - the "referrer" header name is misspelled as
referer
per the specification. Thank you to whoever wrote it out that way originally, as this decision has probably saved a lot of electricity over the years. ↩ -
In the past, you would need to be slightly more clever to ensure a
Referer
isn't sent, but thanks to a privacy feature that modern browsers have adopted, it's as simple as adding areferrerPolicy="no-referrer"
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 sentReferer
headers, and developers thought they could generally assume their presence; various privacy-oriented improvements have ensured that this is no longer the case. ↩
Top comments (0)