Skip to main content

Preventing insecure deserialization in Node.js

blog-feature-playwright-tests

17 de abril de 2023

0 minutos de leitura

Editor's note: May 31, 2023

This post has been updated to show how Snyk Open Source and the Snyk VS Code extension can help you prevent insecure deserialization in your projects.

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

Install the Express web application framework:

npm install express

Install the Node.js serialization package:

npm install node-serialize

Install a cookie parse:

npm install cookie-parser

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...");

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

How many security issues can you point out in the above code? How would you tell if there are any security issues with the code, or with the dependencies we’ve added to the project? 

This is where the Snyk Code extension comes in. It’s free to use and quick to get going. Go to Extensions in VS Code (SHIFT+CMD+X), search for Snyk, install it, and join more than 90,000 developers.

Let me show you a screenshot of Snyk Code in action with the previous Express application code snippet:

Screenshot showing several potential security issues in the Express application server.

Observe all of the purple arrows and how they point out potential security issues in this Express application server. If you look close enough, you’ll spot more issues, and also notice how Snyk Code annotates the lines of code on the `server.js` file demonstrating how insecure code flows between different code paths and code files.

Get the Snyk VS Code extension, it’s free!

Back to Node.js web application, 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

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

blog-insecure-deserialization-cookies

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

eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9

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

{"username":"John","gender":"Male","Age": 35}

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}

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

eyJ1c2VybmFtZSI6IkpvZSBKb25lcyIsImdlbmRlciI6Ik1hbGUiLCJBZ2UiOiA0MH0=

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!");

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");

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

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}

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

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));

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

You should get the following output:

blog-insecure-deserialization-serialize-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

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");

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

node serialize.js

Here’s the output:

blog-insecure-deserialization-serialized

Let’s make sure that the new serialize-javascript suggseted in this article is indeed vulnerability-free as far as CVE reports say. In the npm ls command below you can see that it was added to the project, along-side the vulnerable node-serialize package:

1$ npm ls
2b@1.0.0 /private/tmp/b
3├── cookie-parser@1.4.6
4├── express@4.18.2
5├── node-serialize@0.0.4
6└── serialize-javascript@6.0.1

Then, we scan it with the free snyk npm package:

1$ snyk test
2
3Testing /private/tmp/b...
4
5Tested 62 dependencies for known issues, found 1 issue, 1 vulnerable path.
6
7Issues with no direct upgrade or patch:
8  ✗ Arbitrary Code Execution [Critical Severity][https://security.snyk.io/vuln/npm:node-serialize:20170208] in node-serialize@0.0.4
9    introduced by node-serialize@0.0.4
10  No upgrade or patch available
11
12Organization:      lirantal
13Package manager:   npm
14Target file:       package-lock.json

This shows that the node-serialize npm package is vulnerable, as we’ve demonstrated in this article, as well as that there aren’t publily known security vulnerabilities in the recommended serialize-javascript npm package.

We highly recommend you get started with Snyk Open Source and use the snyk npm package as part of your routine development workflow. Be sure to add it to your CI/CD pipeline to ensure no new security vulnerabilities creep in.

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 Snyk'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 Snyk.io plugin to your IDE.