Why shouldn't JSONP be used?
Older Article
This article was published 8 years ago. Some information may be outdated or no longer applicable.
In a previous article, we covered CORS requests: why they happen and how to deal with them. Sometimes when CORS comes up, people reach for JSONP (JSON with Padding) as a workaround. Let’s talk about why that’s a bad idea.
CORS 101 - a quick recap
When a web application requests a resource from a domain you don’t control, the browser might block you with Failed to load resource: Origin * is not allowed by Access-Control-Allow-Origin.. The browser is refusing access to that resource (typically an API endpoint).
For more information read the article titled “What is CORS?”
An example implementation
Let’s set up a scenario that triggers the CORS problem. Two Node.js files will do it: an API server and an HTTP server.
// api.js
const express = require('express');
const app = express();
const port = 3000;
const apiHandler = (request, response) => {
response.json({ message: 'Hello World!' });
};
app.get('/api/hello', apiHandler);
app.listen(port, () => console.info(`API up on port ${port}.`));
// http-server.js
const http = require('http');
const path = require('path');
const fs = require('fs');
const port = 8080;
const httpServerHandler = (request, response) => {
response.writeHead(200);
response.write(fs.readFileSync(path.join(`${process.cwd()}/index.html`)));
response.end();
};
http.createServer(httpServerHandler).listen(port);
console.log(`HTTP Server is up on port ${port}`);
Please note that this HTTP server is bare-bones. It also uses
fs.readFileSync, a blocking method you shouldn’t use in production. For real work, grab an npm package for your web server.
Now we’ve got two Node.js processes: an API server on port 3000 and an HTTP server on port 8080. Under CORS rules, these count as separate origins, so the browser will block cross-requests.
Here’s a simple HTML file that tries to make an AJAX request via jQuery:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JSONP</title>
<meta name="author" content="Tamas Piros" />
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
</head>
<body>
<h1 id="message"></h1>
<script>
$.ajax({
url: 'http://localhost:3000/api/hello',
}).done((data) => console.log(data));
</script>
</body>
</html>
Open the browser and navigate to localhost:8080. You’ll hit the CORS message. That’s the expected behaviour.
JSONP
Let’s see what JSONP does here. Notice something interesting in our index.html: we’re already pulling in a resource from a different domain (the jQuery library from a CDN).
That works because browsers let you load resources through the <script> tag without CORS restrictions.
JSONP exploits this. We modify our API server to return data wrapped in a function call. That’s the “padding” in JSONP. The browser interprets the function call and we get our data.
Here’s the updated API:
const apiHandler = (request, response) => {
response.jsonp({ message: 'Hello World!' });
};
And the updated AJAX request in index.html:
$.ajax({
url: 'http://localhost:3000/api/hello',
dataType: 'jsonp',
}).done((data) => console.log(data));
Refresh the browser and the console shows the message. We can take it further and push the message into the <h1> tag:
$.ajax({
url: 'http://localhost:3000/api/hello',
dataType: 'jsonp',
}).done(
(data) => (document.getElementById('message').textContent = data.message)
);
It works. But this article is about why you shouldn’t do this.
HTTP GET only
JSONP only works with HTTP GET requests. No other methods. This makes sense: the <script> tag can only fire HTTP GET requests, and that’s exactly what we’ve exploited.
No error handling
With normal AJAX, you can inspect the error body returned by the API. With JSONP, you get either a CORS error or a 404. Debugging becomes a nightmare.
Vulnerability
JSONP opens up security holes. It assumes excessive trust and exposes CSRF (Cross-Site Request Forgery) vulnerabilities.
Bottom line: don’t use JSONP.
Alternatives
If not JSONP, then what? Check out the article “What is CORS?” where several options are covered, including proxies and CORS packages.
For our example, we’ve got two ways to enable CORS on the API server:
// api.js
const apiHandler = (request, response) => {
response.header('Access-Control-Allow-Origin', '*');
response.header('Access-Control-Request-Method', '*');
response.header('Access-Control-Allow-Methods', 'OPTIONS, GET');
response.header('Access-Control-Allow-Headers', '*');
response.json({ message: 'Hello World!' });
};
Please do not forget to remove the
dataType: 'jsonp'property from the AJAX call.
Please note that allowing access to all (
'*') is never a good idea. Be restrictive about who gets access.
Or install the cors package from npm: npm i cors. Once installed, you can add it globally (available to all routes) or as middleware on a single route:
// api.js
const cors = require('cors');
app.use(cors());
// or
app.get('/api/hello', cors(), apiHandler);
Conclusion
JSONP might look like a quick fix for CORS errors, but the downsides outweigh the benefits. The security vulnerabilities alone should steer you towards better options: proper CORS support on your API, or a proxy.