Why shouldn't JSONP​ be used?

In a previous article, we wrote about CORS requests, why they happen and how to avoid them. Sometimes, when discussing CORS requests, people use JSONP (JSON with Padding) as a means to prevent CORS requests to happen. In this article, we'll explain why JSONP is not the best solution and why it should not be used.

CORS 101 - a quick recap

When requesting a resource from another domain that is not under our control from a web application, we may be presented with a message Failed to load resource: Origin * is not allowed by Access-Control-Allow-Origin.. This means that the browser is blocking our request to access a given resource - the resource being an API endpoint.

For more information read the article titled "What is CORS?"

An example implementation

Let's create an example where we can demonstrate the CORS issue - and we can show this very easily by having two Node.js files - 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 a straightforward one and it also uses fs.readFileSync which as a blocking method that should not be used in production systems. In fact, when working with Node.js use an npm package for a web server.

Now we have two Node.js processes - one is an API server that runs on port 3000 and the HTTP server that runs on port 8080. Due to the rules behind CORS, these are considered to be two separate entities, and therefore the browser is going to block accessing resources.

We can demonstrate this by adding a simple HTML file as well that will attempt 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>

Opening the browser and navigating to localhost:8080 will display the CORS message discussed earlier. This is, in fact, the expected behaviour.

JSONP

Let's see what JSONP can do for us in this scenario. First of all notice something very, very interesting in our index.html file. We are in fact bringing in a resource that is not in the same domain that we are (localhost) - that's the jQuery library, which resides on a CDN.

This brings up an interesting point, namely, that we can access resources via the <script> tag.

We need to make some modifications so that our API server returns us the data wrapped in a function call. This is what the "padding" refers to in the abbreviation JSONP. The browser can then interpret this function call.

Let's update the API:

const apiHandler = (request, response) => {
  response.jsonp({ message: 'Hello World!' });
};

As well as the AJAX request in index.html:

$.ajax({
      url: 'http://localhost:3000/api/hello',
      dataType: 'jsonp'
    }).done(data => console.log(data));

If we now refresh the browser, we'll see that the console does, in fact, display the message. We can take this further and add the message to the <h1> tag:

$.ajax({
      url: 'http://localhost:3000/api/hello',
      dataType: 'jsonp'
    }).done(data => document.getElementById('message').textContent = data.message);

At this point we have a working example.

But this article is about discussing why JSONP is a bad thing, why it should not be used.

HTTP GET only

The JSONP way is only applicable to HTTP GET requests, no other method is supported. This is logical since the <script> tag can only make HTTP GET requests anyway. And this is precisely what we have leveraged earlier.

No error handling

Typically when working with AJAX requests we can see the error body returned by the API, however, using JSONP we either get a CORS error or we are faced with a 404 error. Either way, it's incredibly difficult to debug errors when using JSONP.

Vulnerability

JSONP exposes multiple vulnerabilities - it assumes excessive trust, it also further exposes CSRF (Cross-Site Request Forgery) vulnerabilities.

All in all, it is best not to use JSONP.

Alternatives

So if not JSONP, then what can we use? Please review the article "What is CORS?" where a bunch of suggestions are made including the use of proxies and CORS packages.

For our example, we have two options to enable CORS support for our basic 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 with regards to giving access.

Another way is to install a package from npm called cors: npm i cors. Once that's installed we can add it to our API server either globally (available to all routes) or to a single route as a middleware:

// api.js
const cors = require('cors');
app.use(cors());
// or
app.get('/api/hello', cors(), apiHandler);

Conclusion

JSONP could undoubtedly be looked at as a useful way to overcome certain situations that cause CORS errors however there are more negative side-effects to it as well as security vulnerabilities that warrant other methods to be investigated like CORS support for the API or proxies.