Preventing insecure deserialization in Node.js

SnykSec - Apr 24 '23 - - Dev Community

Preventing insecure deserialization in Node.js

This article demonstrates how to patch deserialization vulnerabilities in Node.js. We’ll create vulnerable code, demonstrate an attack, and then fix the vulnerabilities.

Originally published at https://snyk.io/blog/preventing-insecure-deserialization-node-js

Serialization is the process of converting a JavaScript object into a stream of sequential bytes to send over a network or save to a database. Serialization changes the original data format while preserving its state and properties, so we can recreate it as needed. With serialization, we can write complex data to files, databases, and inter-process memory — and send that complex data between components and over networks.

Deserialization is the opposite of serialization — it converts serialized data back into an object in the same state it was before. The problem is that deserialization can be insecure and expose sensitive data. The main cause of insecure deserialization is the failure to protect the deserialization of user inputs. The best way to avoid it is to ensure we only deserialize validated and sanitized data.

Attackers can exploit this vulnerability by loading malicious code into a seemingly harmless serialized object and sending it to an application. If there are no extra security checks, the web application deserializes the received object and executes the malicious code. Alternatively, an attacker can extract sensitive data contained in a serialized object.

The consequences of insecure serialization can be severe. When data is insecurely deserialized by a website, for example, hackers can manipulate serialized objects and pass harmful data into an application’s code. An attacker can even pass a completely different serialized object to access other parts of the application, leaving the application vulnerable to further attacks and data loss.

Insecure deserialization in Node.js

node-serialize and serialize-to-js are Node.js and JavaScript packages that are vulnerable to insecure deserialization. Unlike JSON.parse and JSON.stringify, which only serialize objects in JSON format, these two libraries serialize objects of almost any kind, including functions. This characteristic makes them vulnerable to prototype pollution, an injection attack where a malicious actor gains control of the default values of an object’s properties.

If a prototype pollution attack is successful, an attacker can cause further damage by altering application logic, initiating remote code execution, or a denial of service attack (DoS).

This article demonstrates how to patch deserialization vulnerabilities in Node.js. We’ll create vulnerable code, demonstrate an attack, and then fix the vulnerabilities.

Prerequisites

To follow along with this tutorial, you’ll need:

  • Node.js installed

  • Knowledge of JavaScript

Patching deserialization vulnerabilities in Node.js

Let’s start by setting up a Node.js project. Execute the following command:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Install the Express web application framework:

npm install express
Enter fullscreen mode Exit fullscreen mode

Install the Node.js serialization package:

npm install node-serialize
Enter fullscreen mode Exit fullscreen mode

Install a cookie parse:

npm install cookie-parser
Enter fullscreen mode Exit fullscreen mode

Create a new file server.js and add the code below:

1var express = require('express');
2var cookieParser = require('cookie-parser');
3var escape = require('escape-html');
4var serialize = require('node-serialize');
5var app = express();
6app.use(cookieParser())
7
8app.get('/', function(req, res) {
9   if (req.cookies.profile) {
10      var str = new Buffer(req.cookies.profile, 'base64').toString();
11      var obj = serialize.unserialize(str);
12      if (obj.username) {
13          res.send("Hello " + escape(obj.username));
14      }
15  } else {
16      res.cookie('profile', "eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9", {
17          maxAge: 900000,
18          httpOnly: true
19      });
20  }
21  res.send("Welcome to the Serialize-Deserialize Demo!");
22});
23app.listen(3000);
24console.log("Listening on port 3000...");
Enter fullscreen mode Exit fullscreen mode

In the file above, we’re using the insecure module node-serialize. We’re also passing untrusted data to its function, unserialize().

The outcome is that the endpoint, app.get(‘/’, function(req, res), is made vulnerable to insecure deserialization. That’s because the application adds users’ inputs to a preference cookie value that’s already serialized.

To demonstrate the vulnerability, run the express server by executing this command:

node server.js
Enter fullscreen mode Exit fullscreen mode

This starts the server at port 3000. Open http://localhost:3000/ to view it and inspect the cookies, as shown below:

Now, convert the cookie from base64 format into JSON format. You can do this using an online tool like Code Beatify. This is the cookie in base64:

eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9
Enter fullscreen mode Exit fullscreen mode

And this is the cookie that the server sets after we convert it from base64 into JSON:

{"username":"John","gender":"Male","Age": 35}
Enter fullscreen mode Exit fullscreen mode

To view the deserialization problem at work, let’s change the value of the JSON object and encode it into base64, replacing the cookie value in the browser with the new cookie.

Change the JSON object to:

{"username":"Joe Jones","gender":"Male","Age": 40}
Enter fullscreen mode Exit fullscreen mode

Open this Base64 tool to encode the JSON. When encoded to base64, this becomes:

eyJ1c2VybmFtZSI6IkpvZSBKb25lcyIsImdlbmRlciI6Ik1hbGUiLCJBZ2UiOiA0MH0=
Enter fullscreen mode Exit fullscreen mode

Now, replace the current cookie value in the browser with the encoded value of the new object, and edit the value of the HTTP response in the server.js file to the following:

res.send("Welcome to the Insecure Deserialize Demo!");
Enter fullscreen mode Exit fullscreen mode

When you restart the connection and refresh the page, you’ll see the new response.

Exploiting the vulnerability

Let’s exploit the vulnerability to perform an arbitrary code execution (ACE), which passes untrusted data to the unserialize() function.

First, we’re going to create a payload with the serialize() function in the same module we’ve been working with. To do this, create a file named serialize.js and add the following code to it:

1var serialize = require('node-serialize');
2var m = {
3   myOutput: function() {
4       return 'Hello';
5   }
6}
7console.log("Serialized: \n" + serialize.serialize(m) + "\n");
Enter fullscreen mode Exit fullscreen mode

This is the Node.js script that will serialize our code.

Now, open a terminal, navigate to the root folder, and execute this command:

node serialize.js
Enter fullscreen mode Exit fullscreen mode

Replace the username parameter in the cookie value with the value of the myOutput parameter in the output above. Our new object that’s in the cookie should now look like this:

1{"username":"_$$ND_FUNC$$_function(){ return 'Hello'; }","gender":"Male","Age": 40}
Enter fullscreen mode Exit fullscreen mode

The output above gives us a serialized payload to pass to the unserialize()function. To achieve ACE, we have to use a JavaScript Immediately Invoked Function Expression (IIFE) to call the function.

When we add the IIFE bracket, (), after the function body of this serialized payload, the function will execute after object creation. Below is the JSON object that will now be stored in the cookie.

1{
2   "username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
3   "gender": "Male",
4   "Age": 40
5};
6
Enter fullscreen mode Exit fullscreen mode

To demonstrate how this manipulated object introduces dynamic code evaluation to a running Node.js server, we’ll use it as an example in our utility serialize.js file. Let’s pass this payload to an unserialize() function. Modify the seralize.js file by adding the following code:

1var serialize = require('node-serialize');
2var m = {
3   myOutput: function() {
4       return 'Hello';
5   }
6}
7
8console.log("Serialized: \n" + serialize.serialize(m) + "\n");
9
10var km = {
11  "username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
12  "gender": "Male",
13  "Age": 40
14};
15console.log(serialize.unserialize(km));
Enter fullscreen mode Exit fullscreen mode

In the code above, we added a new variable, km, that contains a function as the value of username, which we passed to the unserialize() function.

Now, run the utility script file to check whether the function is executed with the following command:

node serialize.js
Enter fullscreen mode Exit fullscreen mode

You should get the following output:

The serialization process was successfully exploited! As attackers who control the input of the client-side object, we were able to manipulate it to include JavaScript code that changes the username field.

Fixing the insecure deserialization vulnerability

The best way to prevent insecure deserialization is to avoid deserializing user inputs completely. The second best way is to check the user input before serializing it. We can use a package called Serialize JavaScript to sanitize the inputs in the vulnerable example above.

First, install serialize-javaScript using npm:

npm install serialize-javascript
Enter fullscreen mode Exit fullscreen mode

Modify the serialize.js file by replacing its code with this:

1var serialize = require('serialize-javascript');
2var m = {
3   myOutput: function() {
4       return 'Hello';
5   }
6}
7console.log("Serialized: \n" + serialize(m, {
8   ignoreFunction: true
9}) + "\n");
Enter fullscreen mode Exit fullscreen mode

The ignoreFunction ensures that the functions used to execute ACE are not serialized. Run the file to confirm this:

node serialize.js
Enter fullscreen mode Exit fullscreen mode

Here’s the output:

Best practices to avoid deserialization vulnerabilities

Deserializing user inputs can allow malicious actors to attack a system and expose sensitive data. The best way to protect against insecure deserialization is to avoid deserializing data from untrusted sources. But if we need to deserialize data, we should implement additional security measures to verify the data has not been manipulated.

Additionally, we should sanitize inputs. We can do this by checking whether the object to serialize contains functions and stopping the serialization if it does.

Secure deserialization

Serialization and deserialization are critical processes that allow seamless data transfer over a network. However, insecure deserialization can lead to critical vulnerabilities. By injecting hostile serialized objects into a web app, typically in the form of user-input data that is then deserialized, attackers can pass harmful data to an application. They can then launch an injection attack, remote code execution, or a DDoS attack.

We can prevent attacks caused by this vulnerability by simply not deserializing user inputs. If the deserialization must occur, we should include additional security mechanisms, such as anti-forgery tokens, to ensure the data hasn’t been modified.

Visit Synk’s blog to learn more about preventing insecure serialization and deserialization vulnerabilities in Java.

To secure your code from insecure serialization and deserialization vulnerabilities, add the Synk.io plugin to your IDE.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .