Vulnerabilities in NodeJS C/C++ add-on extensions
Alessio Della Libera
2024年8月14日
0 分で読めますOne of the main goals of this research was to explore C/C++ vulnerabilities in the context of NodeJS npm packages. The focus will be on exploring and identifying classic vulnerabilities like Buffer Overflow, Denial of Service (process crash, unchecked types), and Memory Leakages in the context of NodeJS C/C++ addons and modeling relevant sources, sinks, and sanitizers using Snyk Code (see Snyk brings developer-first AppSec approach to C/C++).
The targets for this research are NPM packages that use C/C++ interfaces as part of their implementation. We haven’t targeted projects that are not listed on NPM.
In this blog post, we aim to provide an overview of common security vulnerabilities and vulnerable patterns that can occur when writing C/C++ add-ons in NodeJS. We’ll also provide remediation examples and suggestions for open source maintainers.
This blog post was inspired by the paper “Bilingual Problems: Studying the Security Risks Incurred by Native Extensions in Scripting Languages” by Cristian-Alexandru Staicu, Sazzadur Rahaman, Àgnes Kiss, and Michael Backes.[1] In their original paper, the authors provided an analysis of the security risk of native extensions in popular languages, including JavaScript.
NodeJS C/C++ add-ons background
NodeJS provides different APIs to call native C/C++ code. The scope of this research is to investigate security vulnerabilities that could occur when using one of the following mechanisms:
node_api.h: Node-APInapi.h: C++ wrapper around Node-API
A good resource that provides examples of using the libraries above can be found in GitHub.
For a complete introduction to add-ons and how to build them, refer to NodeJS's official documentation.
The vulnerabilities covered and identified in at least one package are:
Memory leaks
Unchecked type (DoS)
Reachable assertion (DoS)
Unhandled exceptions (DoS)
Buffer overflow
Integer overflow
In the following sections, examples of vulnerable patterns will be provided with also an explanation of the conditions to be satisfied in order to make the vulnerability exploitable.
Examples of vulnerable patterns
In this section, we are going to explore how add-on-specific APIs can lead to security issues if not properly handled and some vulnerable patterns identified as part of this study.
NOTE: The following examples do not represent a comprehensive list. There might be more scenarios
that can lead to security issues not covered in this blog post.
Setup
Install node-gyp (https://github.com/nodejs/node-gyp).
The following files are used to run the examples in the next section:
package.json
binding.gyp
Run the following commands to build the C/C++ extensions:
node-gyp configurenode-gyp build
Run specific example:
main.js
Unhandled exceptions
Impact: Denial of Service (DoS)
napi
The napi API provides different functions to handle exceptions and throw errors. However, depending on the flag used in the binding.gyp file, some attention needs to be taken in order to avoid unexpected crashes.
For example, if the flag NAPI_DISABLE_CPP_EXCEPTIONS is set in the binding.gyp file, the following scenarios can lead to a process crash (DoS):
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();in addition to other functions that can generate an error (for example, wrong type argument)throw Napi::Error::Newnot surrounded bytry/catchMultiple
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();withoutreturnthat can be reached within the same function
As explained in the docs, “after throwing a JavaScript exception, the code should generally return immediately from the native callback, after performing any necessary cleanup.” .
test_napi_exceptions.cpp
Run these examples:
Reachable assert
Impact: Denial of Service (DoS)
node_api
Looking at the provided examples, we can see that in some examples , assert is used to check the return value of some functions. However, if an assert is reached by tainted values (from the javascript code) during the program execution, it can lead to a crash (DoS). While reviewing some projects, we found several occurrences of reachable asserts in the code logic, so I thought it’s worth mentioning as part of the previous list.
A possible fix for this scenario would be to check the return value inside an if and then return the appropriate value (depending on the logic of the program), instead of using an assert.
test_node_api_assert.c
Run this example:
Unchecked data type
Impact: Denial of Service (DoS)
napi
napi provides several APIs to coerce JavaScript types. For example,
Napi::Value::ToString() “returns the Napi::Value coerced to a JavaScript string.” Similarly, Napi::Value::ToNumber() “returns the Napi::Value coerced to a JavaScript number.”
The napi Napi::Value::ToString() API, under the hood calls napi_coerce_to_string from Node-API:
Similarly, the napi Napi::Value::ToNumber() API, under the hood calls napi_coerce_to_number from Node-API :
From the official docs for napi_coerce_to_string: “This API implements the abstract operation ToString() as defined in Section 7.1.13 of the ECMAScript Language Specification. This function potentially runs JS code if the passed-in value is an object.” This means that if the user input defines a toString property, the value of that property will be returned (instead of calling the toString()), leading to unexpected results.
If we call other methods on the values returned by Napi::Value::ToString(), and the input defines a property toString, we can occur in an exception, most of the time leading to the process crash. The same holds for napi_coerce_to_number.
Vulnerable pattern:
calls like
Napi::String::Utf8Value()on anNapi::Valueresulted fromToString()orToNumberwithout proper type checking
A possible remediation to avoid these scenarios, is to check if the value returned from Napi::Value::ToString() or Napi::Value::ToNumber() are, respectively, string or number before calling other methods on these values.
NOTE: Like the unhandled exceptions cases mentioned previously, these issues occur if the flag NAPI_DISABLE_CPP_EXCEPTIONS is set in the binding.gyp file.
test_napi_unchecked_type.cpp
Run these examples:
Memory leaks
Impact: Information Disclosure
napi
The napi API provides several methods to create a JavaScript string value from a UTF8, UTF16-LE or ISO-8859-1 encoded C string. These APIs are:
All these methods have the same signature:
The interesting value to carefully check is the [in] length, that is, the length of the string in bytes. If this value is controlled by an attacker or is hardcoded and the input value is tainted, then it’s possible to store in the result value, unexpected memory values.
To avoid such problems, use NAPI_AUTO_LENGTH for the size_t length value.
Vulnerable pattern:
napi_create_string_*withsize_t lengthgreater than the length of theconst char* str
test_napi_memory_leak.c
Run this example:
Methodology
To test and find as many issues as possible automatically, I used the following approach to leverage the power of Snyk Code:
Create a dataset of npm packages that calls C/C++ using NodeJS add-on APIs
Write security rules in Snyk Code to model:
Sources: in this context, sources are values coming from JavaScript code, that could be data coming
Napi::CallbackInfo::Env()in the context ofnapi- ornapi_get_value_*- in the context ofnode_apiSinks: depending on the security issue, I modeled the presence of multiple
ThrowAsJavaScriptExceptioncalls within the same function, theassertcheck, and several methods used to create string values (just to name a few). I also took into account situations where the code is not vulnerable because of the presence of some arguments likeNAPI_AUTO_LENGTHin case of Memory Leak issues
Write rules that use the sink and sources defined to perform a taint analysis, to track taint from sources to sink
Use the sources defined in the the existing rules we support (for example, Buffer Overflow or Integer Overflow), so that I can cover even more C/C++ vulnerabilities (not only those specific that use NodeJS add-ons APIs)
Run these rules against the previously built dataset
Manually review the results and eventually build a PoC
Using this approach, I was able to find several issues in npm packages by modeling the relevant APIs related to the NodeJS add-ons by using Snyk Code.
However, for some of the issues found, I sampled some projects from the dataset build and manually reviewed them.
Outcomes
Multiple vulnerabilities in packages were found as a result of this research. These can be found below
Conclusion
On a personal note, this research was an incredible learning experience for several reasons. I had the opportunity to deep-dive into the world of NodeJS add-ons, review existing literature about existing issues, and try to model some scenarios using Snyk Code to find issues in a large set of repositories.
While I’m pretty familiar with JavaScript and many other languages, C/C++ is a language that I recently started learning due to the work we did (and are still doing) to support multiple security rules that are now available to Snyk Code customers. Combining both aspects, learning experience and the opportunity to use Snyk Code to model several security issues, I really enjoyed this research, and for this, I want to thank Snyk for the opportunity provided.
References
Node-API - https://nodejs.org/api/n-api.html
node-addon-api - https://github.com/nodejs/node-addon-api
C++ addons - https://nodejs.org/api/addons.html
