Exploiting Number Parsers in JavaScript

Summary

This research will explore the following topics:

  • An overview of number parsers in JavaScript
  • Writing security tests to identify unusual behaviors
  • Exploitation scenarios
    • parseInt & parseFloat
    • numeraljs and format injection
  • Prevention methods & conclusion

An overview of number parsers in JavaScript

There are two general ways to parse numbers in JavaScript: using the Number Constructor and its static methods or using npm packages like numeral or parse-int.

1. Number Constructor

The Number constructor in JavaScript is a built-in function that can create a number object. While it is often used as a simple function to convert values to numbers, it can also be called as a constructor to create an instance of a Number object. “When Number() is called as a function (without new), it returns value coerced to a number primitive. Specially, BigInts values are converted to numbers instead of throwing. If value is absent, it becomes 0. When Number() is called as a constructor (with new), it uses the coercion process above and returns a wrapping Number object, which is not a primitive. More info about Number constructor

parseInt and parseFloat are static methods used to convert strings to numbers:

  • string: The value to parse. If the first character cannot be converted to a number, parseInt() returns NaN.
  • radix (optional): An integer between 2 and 36 that represents the base of the numeral system to be used. If omitted, JavaScript assumes a base of 10 (decimal) unless the string begins with 0x (for hexadecimal) or 0 (for octal in older browsers).

2. Numeral

The numeral is an npm package that provides a simple way to format and manipulate numbers. It can parse strings into numbers, which can be useful when retrieving numerical input:

http://numeraljs.com/

3. parse-int

The parse-int package is a small library designed to provide a more controlled way of parsing integers from strings, expanding on the functionality provided by the native parseInt function.

Writing Security Tests to Identify Unusual Behaviors
Due to my interest in writing tests, I aimed to write tests from a security perspective to gain better control over the logic and types of input I want to pass to the parser. Of course, besides this approach, you can also use other methods for fuzzing.

For certain types, like arrays or objects, I expected the output from these parsers to be undefined or NaN. After further investigation, I noticed some interesting outputs, which we’ll review below.

1. Writing Security Tests for parseInt and parseFloat

I provided a list of different data types as input for the parser and examined the output value along with its type. I expected the output to be a number for these inputs, and if the test passed, it indicated that for certain non-number types, the output was still being generated as a number. Observing this output, it was either NaN or indeed a number:

This test passed, and I had the following output:
When an array with more than one element is passed to parseInt or parseFloat, the output we receive is the first element of the array. The question arises: if a validator checks only the range of the input (not its type) before sending it to these methods, what issues might arise? This will be covered further in the attack scenarios. The summary of results obtained so far is shown in the image below:

2. Writing Security Tests for numeral

In this package, we can use different formats for displaying numbers. My purpose in testing numeral was to see what outputs might be generated with various formats or undocumented cases. In the image below, we see the test results:

One interesting finding was that when a string of numbers separated by the + character is passed to numeral, the output is the concatenated result as a number. Other cases in the image show the default formats supported by numeral.

I tried writing similar tests for parse-int, and this package handled different types well, returning undefined for non-numeric types.

Exploitation Scenarios

  1. parseInt & parseFloat

First, we’ll go over an example together, and then we’ll explore real scenarios I’ve encountered.

The application has a feature where users can view a list of users in a paginated format, limited to 200 users per page. The API call to fetch this data is structured as follows:

POST  /api/products

{“page”:1, “size”:50}

Imagine if, when the above request is sent, the size parameter is first checked by a validator to ensure it’s within a specific range (e.g., between 100 and 200). Consider the following pseudocode:

What happens here is that after retrieving the size field from the body, this value is checked by a validator to see if it falls within a specified range. If it passes, the field is then parsed by parseInt, and ultimately, a query is made to the database.

Now, let’s say instead of passing the value 200, we send an array like [20000, 300]. Inside the validator, when an array is compared with a number, this condition fails, effectively exiting the function. Then, the array is passed to parseInt, and as previously reviewed, the first element (20000) is returned and stored in newSize to be used in the database query. Given the large value of newSize and the number of records that could be returned, we would encounter a DoS:

Case Study During Code Review

One instance occurred during a code review of an application with a structure similar to the following:

While reviewing one of the routers, I came across the following implementation:

The parameters page, limit, and userId are read from the query string, and these values are checked by validators. The interesting one is inRange. First, I reviewed the logic used to validate the limit and page:

The above condition checks whether value falls within a specified range. The issue here is that the type of value is not checked, allowing us to send an array that fails this condition. I wrote the following test as a proof of concept:

In the first case, I expected an error to be thrown if a number outside the range was passed. In the second, I expected no error if an array, such as [100, 200], was passed, as the condition in inRange would fail, and thus no error would be thrown. The following result was observed after executing the test:

Next, I needed to verify whether limit or page was passed to parseInt or parseFloat later in the code. In the third step, we see the logic used for getTransactions. Here, parseInt is used to parse the limit and page parameters:

To exploit this vulnerability, an array would need to be sent as a query string, so that when parseInt returns the first array element, it results in a DoS for the application. How can we send an array as a query string? By using parameter pollution in Node.js.

In Node.js, duplicating a parameter in the query string results in the parameters being interpreted as an array. To exploit this, we can use the following flow:

In the next step, I attempted to find all vulnerable routers in the code with semgrep. To write a custom rule, I first reviewed the structure of vulnerable validators in the code, finding another validator named greaterThan with a similar issue:

To write a custom rule, we need to identify routers where inRange or greaterThan are used to validate input and ensure that the code uses parseInt or parseFloat for parsing inputs, focusing on cases where parameters are read from either the query string or request body. In the first case, using the query string, we can exploit it through parameter pollution, and in the second case, we simply need to place an array in the JSON body and send it:

To verify which routers from this scan result are truly vulnerable, I needed to consider an important point. Routers that first use parseInt or parseFloat to parse the input and then pass it to the validator are not vulnerable. Even if we send an array as input in this case, it will first be parsed by parseInt, and only the first element will be passed to the validator. This prevents our attack scenario from working, as we’re looking for a way to falsify the validator condition. Let’s take a look at following example:

The next point I had to consider was that after the validator is called, the input might be indirectly passed to parseInt. After further investigation, I found that some services in the application use another custom validator called parseData. Let’s take a look at this validator:

My goal was to bypass this validator and get the input into parseInt. To do this, the condition in the ternary operator needs to be true so that the array we send is passed to parseInt. Here, I analyzed the conditions. For the array to be passed to parseInt, one of the two conditions must be true.
When we send an array, the first condition, !(isNaN(id)), evaluates to false, so we need to make the second condition true. When the toString method is called on an array, all elements of the array are returned as a string, joined with the “,” character:

The length of the array, including the “,” character, should be equal to 24 for our condition to be true. To achieve this, we set the first element of the array to the number we intend to use in the query later, and we fill the rest of the array with random numbers. This makes the condition true, and the input is passed to parseInt:

The general attack scenarios are shown below:

I was able to identify 12 vulnerable routers. Below is an example of exploiting this vulnerability in one of these routers:

2. Numeral Package & Format Injection
In some applications, the numeral package is used for parsing user inputs. Numeral supports various formats, as mentioned earlier. When testing application inputs, different parsers might be used in the back-end. Without input validation or format restrictions, injecting various formats and fuzzing inputs could lead to DoS. Here’s an example attack flow when numeral is used for parsing user input:

Consider the following code snippet:

Assuming there is no validation on the different formats received as input, we can achieve a DoS by sending or fuzzing various formats. An example request is shown below:

Prevention Methods

  • Always check data types before using user input and ensure that the data used in a validator is of type number; include this in conditions you use.
  • When using packages like numeral, define a whitelist of acceptable formats to prevent users from entering numbers in arbitrary formats.

Conclusion

In this research, I analyzed the parsers used for handling numbers in JavaScript and explore different scenarios together. By applying similar scenarios in other languages and parsers, you might encounter interesting cases.

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *