I learned to write computer programs before JavaScript was created. The languages used in schools back then were mainly C and Pascal. They taught me that each variable has a specific type, such as integer or string, and that this type determines the operations that can be performed on a variable.
JavaScript is a bit different.
Types
JavaScript also has types. Variables can refer to numbers, strings, Boolean values, objects, symbols, and special values such as undefined
and null
.
Dynamic typing
Unlike C and Pascal, JavaScript variables can hold values of different types throughout their lifetime. A variable can be a number in one execution scenario and a string in another one. This makes it difficult to analyze how the program works just by reading its source code.
Weak typing
Operators work on values. For example, the +
operator adds two numbers together or concatenates two strings. In C and Pascal, you cannot add a number to a string. This operation is undefined, and you need to convert one of the variables to a different type.
JavaScript will do its best to convert the operands implicitly, often in surprising ways.
Comparing objects of different types
JavaScript has two comparison operators:
-
Strict comparison (
===
) compares both the value and the type. If the compared values have different types, it will returnfalse
. This is what we would intuitively expect from a comparison operator. -
Loose comparison (
==
) tries to automatically convert the operands to a common type to make the comparison possible. The rules of the conversions are complex and can be confusing for newcomers. Who would expect that the special valuenull
can be equal to another special valueundefined
?
Both dynamic and weak typing allow JavaScript programs to be very flexible and succinct, but they may also lead to security problems.
Searching based on dynamic criteria
The dynamic nature of JavaScript makes it possible to implement algorithms that work on different types of data, including objects with different properties.
Let’s try to implement an HTTP endpoint that allows searching for objects in an array based on an arbitrary field and value and see how the type system can help us make the code as generic as possible. This will help us reuse it for different types of objects, and different types of search fields.
Our sample will use the Express framework to deal with details of handling HTTP requests but you don’t need to know Express in-depth to understand the code.
Search example
In our example, we will search the array of objects representing users. The search parameters will be passed as query string parameters. The callers will pass an object property name in the field
parameter, and the search value in the value
parameter. This way one endpoint can support multiple different search criteria.
The sample HTTP request and response could look like this:
GET /profile?field=email&value=joe%40wiredbraincoffee.com HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Accept: */*
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 120
Connection: keep-alive
[{"email":"joe@wiredbraincoffee.com","password":"coldbrew","address":"1235 Wired Brain Blvd\r\nAwesome City, MM 55555"}]
Handler
The HTTP handler code is quite generic:
const users = require('./users');
function readProfile(req, res) {
// Get search params
const [field, value] = getParams(req.query, ['field', 'value']);
// Find user(s)
const results = filter(users, field, value);
res.json(results);
}
First, we import the users
array from a separate module. The readProfile
function implements the search algorithm and conforms to Express conventions of taking the HTTP request and response objects as parameters.
Here is where the fun begins: we grab the values of field
and value
query string parameters and use those values to search the users
array to find objects that have the property stored in the field
variable with the value equal to the value variable.
Utility functions
The readProfile
implementation looks simple, but the bulk of the work happens in the filter
function:
// Return items where a field has specific value
function filter(items, field, value) {
const results = [];
for (let i = 0; i < items.length; ++i) {
if (items[i][field] == value) {
results.push(items[i]);
}
}
return results;
}
The filter
function iterates over each element of the array and uses the bracket notation to retrieve the object property by name. The algorithm uses the loose comparison operator to compare object property value to the search criteria provided by the user.
// Retrieve array of parameters from the query string
function getParams(qs, params) {
const results = [];
for (let i = 0; i < params.length; ++i) {
const value = qs.hasOwnProperty(params[i])
? qs[params[i]]
: null;
results.push(value);
}
return results;
}
The getParams
function streamlines retrieval of search parameters from the query string. It takes an array of parameter names as an argument and iterates over it. For each parameter, it checks if it is present in the query string and adds it to the results array. If the requested parameter is not in the query string, it adds null
instead. null
is a special JavaScript value used to denote missing data.
The resulting code is short and can easily be reused to implement search over other data sets, and based on criteria provided by the caller at runtime.
It also has a security flaw.
Abusing loose comparison
One of the surprising rules that the loose comparison operator uses to compare values of different types is the one that says that null
and undefined
are equal, while the strict comparison algorithm treats those two values as different.
Let’s take one more look at the comparison in the filter function:
if (items[i][field] == value) {
If we were able to force one operand to be always null
, and the other to always be undefined
, the comparison would always return true. Our HTTP endpoint would return the entire content of the users array, disclosing sensitive information about all the users of our application.
How can we do that?
Attack payload
The right-hand side of the comparison is a value returned by the getParams
function. We can for this value to be null
by… omitting it from the query string altogether.
Now we need a way to get the left-hand side to always return undefined
. undefined
is a special value that JavaScript uses for variables and object properties that have not been written to. If the field variable referred to a property that does not exist, the entire left-hand side of the comparison would always return undefined
.
We do not always know what properties exist on objects. With a little bit of trial and error, it should not be difficult to find a value that is very unlikely to be a real property name.
A successful attack could look like this:
GET /profile?field=doesnotexist HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Accept: */*
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 364
Connection: keep-alive
[{"email":"janet@wiredbraincoffee.com","password":"coldbrew","address":"1234 Wired Brain Blvd\r\nAwesome City, MM 55555"},{"email":"joe@wiredbraincoffee.com","password":"coldbrew","address":"1235 Wired Brain Blvd\r\nAwesome City, MM 55555"},{"email":"michael@wiredbraincoffee.com","password":"coldbrew","address":"1236 Wired Brain Blvd\r\nAwesome City, MM 55555"}]
The fix
The root cause of the vulnerability is not difficult to fix. The ===
operator will treat undefined
and null
as different values. The comparison will always return false and the endpoint will not return any data from the users
array, as expected.
This simple change fixed the vulnerability but there is more than we can do.
A more robust fix
The vulnerability was exploitable because of the loose comparison and the fact that the attacker could omit the value parameter. Instead of returning an error, the readProfile function was executed with corrupt input data.
A more complete fix uses the ===
operator but also adds more strict input validation. Our endpoint should return HTTP 400 response code when query string parameters are:
- Missing. Omitting a parameter can lead to unexpected code behavior. The dynamic and weak typing make our program work without errors, even if it does something we did not expect it to do.
-
Invalid. We also need to validate if the values are within the expected range. In our example, we should do it for the
field
parameter: we know what properties objects from the users array have, and there is no reason to allow other values.
We will leave adding this input validation logic as an exercise for… you, dear reader. Have fun!
What's next?
The next post in this series will explain how using certain unsafe functions can allow attackers to execute their code within our applications.
Top comments (0)