The dangers of square bracket notation

Dot notation is the most common solution for accessing object properties, but if you want to dynamically access a specific property, square bracket notation is your go-to. Discover how to use the square bracket notation without the fear of attacks.

Reading Time6 minutes

Accessing object properties is one of the first things you learn as a programmer. At that point, only a few know what kind of issues one of the accessing methods can cause.

We will be looking at some potentially dangerous implications of JavaScript’s square bracket notation along with the object constructor in this blog: specifically when it could happen and what we could do to prevent potentially dangerous cases. 

But let’s start by explaining what the square bracket notation is.

Square bracket notation

The square bracket notation, alongside the dot notation, is a property accessor providing access to an object’s properties.

const user = {
       name; 'John'
       lastname; 'Smith'
}

console.log(user.lastname)
// output: 'Smith'

console.log(user['lastname'])
// output: 'Smith'

const prop = 'lastname'
console.log(user[prop])
// output: 'Smith'

The square bracket notation for objects in JavaScript provides a very convenient way to dynamically access a specific property or method based on the contents of a variable. 

For example, we often get an array of objects of countries that we transform into an object to reduce the complexity when accessing an individual object. Let’s take a look at the following example. Imagine that we have a web page containing a select input with a list of countries as options. When we select one, the countryCode variable is changed. Later, we use that variable to get full info from countries’ objects by using bracket notation in combination with a user-controlled variable.

const counties = {
 AF: {
   name:'Afghanistan',
   code: 'AF'
},
 AL: {
   name: 'Albania',
   code: 'AL'
 },
 // … 
}

let countryCode = 'AF' // assuming this is user-controlled, via select
let country = counties[countryCode]

The square bracket notation, in itself, is a fairly common way of accessing an object’s properties, but the issue occurs when we use bracket notation with a variable that is controlled by the user. This type of accessing an object’s properties grants access to every property available on the object, including prototypes, which can lead to different types of attacks, like injections, prototype pollution, etc. To explain the issue, we will focus on code injection examples only.

Code injection as one of the issues

Code injection is the general term for attack types which consist of injecting code that is then interpreted/executed by the application. This type of attack exploits poor handling of untrusted data. These types of attacks are usually made possible due to a lack of proper input/output data validation, such as allowed characters, data format, or the amount of expected data.

const express = require('express');
const bodyParser = require('body-parser');

(async () => {
	const app = express();
	app.use(bodyParser.urlencoded({ extended: true }));

app.post('/change-phone', function requestHandler(req, res) {
		// happens when user sends post request
		handlePhoneNumber([req.body.key, req.body.value])
		res.sendStatus(200)
});

	const server = await app.listen(3000);

	var user = function () {
		this.name = 'john';
// an empty user constructor.

	};

	function handlePhoneNumber(userInput) {
		// changing user’s phone number -> this is an issue
		// this can be any attribute, and does not need to be user controlled.


	user['phone'] = user[userInput[0]](userInput[1]);
	}
})()

In this case, user can send post request that looks like this:

curl -d "key=constructor&value=require('child_process').exec(arguments[0], console.log)" -X POST http://localhost:3000/change-phone

The constructor property returns a reference to the object constructor function that created the instance object. Any object (with the exception of objects created with Object.create(null)) will have a constructor property on its [[Prototype]], and, in our case, the constructor property is corrupted due to code injection. After this, users can carry out remote code execution:

curl -d "key=phone&value=cat /etc/passwd" -X POST http://localhost:3000/change-phone

How to avoid this?

The most direct fix that might cross our minds is not to use the user’s input in the property name fields. However, the square brackets notation is one of the core language features and there should be a safe way to use it. Furthermore, sometimes, as shown in the above example, there is a reasonable need to use bracket notation.

Alongside proper input/output data validation, we could create a whitelist of allowed property names, then filter each user’s input with a helper function to check it before allowing it to be used. This is a simple solution when we already know specifically what property names are allowed. In cases where we don’t have a defined data model, we could use a similar method: blacklist disallowed properties.

The third option I came across while researching is a function that Lodash used in its code:

function safeGet(object, key) {
 if (key === 'constructor' && typeof object[key] === 'function') {
   return;
 }

 if (key == '__proto__') {
   return;
 }
 
 return object[key];
}

Square bracket notation or dot notation?

Even though the dot notation is more common, sometimes there is a rational need for the square bracket notation. In most of those cases, we need square brackets because it is a way to use the variable to access objects’ properties. When it comes to it, we should be aware of the potential danger that the combination of the square bracket notation and object constructor brings to the table. One of those dangerous scenarios is remote code execution which is hard to detect because there is very little indication in the code that this is happening. 

A useful tool for detecting such issues is Semgrep - this tool analyzes source code to find security vulnerabilities that make your applications susceptible to attack. As mentioned before, there are a few possible options that will let us use this syntax, but prevent the user from carrying out remote code execution.

Your take on the subject

They say knowledge has power only if you pass it on - we hope our blog post gave you valuable insight.

If you want to share your opinion or learn more about what our developers can do for your business, feel free to contact us. We'd love to hear what you have to say!