-- MARKDOWN --
- Internals of JavaScript/TypeScript's most popular utility libraries can be attacked with special attributes of processed user-supplied objects, such as `__wrapped__`, `@@functional/placeholder` or `length`
- Exploitability is dependent on how the libraries are used, i.e. which function parameters are filled with user-supplied and potentially malicious values
- We show DoS (high probability of vulnerable code patterns), control flow manipulation (medium probability) and RCE (highly unlikely to occur organically, but could be abused to obscure backdoors)
- The findings paired with mitigation suggestions were privately disclosed to the library maintainers first; Lodash did not respond, Ramda and Underscore decided not to implement any fixes, stating that developers must take care of sanitization instead
- This post gives an introduction to the affected internal processes, showcases some of the potentially vulnerable code patterns including possible exploits and provides pointers to enable developers to safeguard against these issues
# Table of contents
- [Introduction](#introduction)
- [Lodash](#lodash)
- [Special attributes](#lodash-special-attributes)
- [DoS](#lodash-dos-lodash)
- [Control flow manipulation](#lodash-control-flow-manipulation)
- [RCE](#lodash-rce)
- [Bonus RCE](#lodash-bonus-rce)
- [How to prevent vulnerabilities](#how-to-prevent-vulnerabilities-in-lodash)
- [Ramda](#ramda)
- [Special attributes](#ramda-special-attributes)
- [DoS](#ramda-dos)
- [Control flow manipulation](#ramda-control-flow-manipulation)
- [RCE](#ramda-rce)
- [How to prevent vulnerabilities](#how-to-prevent-vulnerabilities-in-ramda)
- [Underscore](#underscore)
- [Special attributes](#underscore-special-attributes)
- [DoS](#underscore-dos)
- [How to prevent vulnerabilities](#how-to-prevent-vulnerabilities-in-underscore)
- [Conclusion](#conclusion)
- [Timeline](#timeline)
# Introduction
Utility libraries provide well tested implementations for functions and procedures commonly required or desired in software development.
The three libraries discussed in this post all saw their initial releases more than 10 years ago in a time when support for even the most basic functional programming primitives such as `Array.forEach` and `Array.map` was not yet widely available in vanilla JavaScript.
In the last decade and a half, JavaScript and TypeScript have also established themselves as popular languages for backend applications which must process large amounts of untrusted user inputs. This has opened up attack vectors to the internals of these libraries which may not have been considered during their inception.
The potential impact of exploiting the vulnerabilities described here also vastly depends on the context:
- For any application that does not process untrusted data submitted by other users, no negative impact should be expected
- Web frontends that display or process untrusted data could crash the victim's browser or suffer from XSS
- In case of a NodeJS web application backend however, these vulnerabilities could be abused to crash a server, bypass authorization checks or fully compromise the server with all of its data
As mentioned in the TL;DR, the XSS/RCE case is very unlikely to occur naturally.
This post is meant to educate and potentially give basis for a civilized discussion.
We strongly disapprove of angry and entitled behavior towards anyone contributing time or resources to FOSS in good faith.
**Interactive challenges/demos**
- This page embeds interactive sandboxed JavaScript playgrounds for each of the issues we describe which are best viewed on a large format screen
- Each playground is pre-populated with a non-malicious user input in JSON format and a vulnerable code snippet from a ficticious application relying on a specific utility library
- The examples can be used as little hacking challenges where only the `userInput` textarea should be used to input a JSON payload that achieves the goal
- The more difficult challenges are quite convoluted and not intended to be solved casually while reading
- **Feel free to skip the challenges and instead transform them into demos by clicking the blue "Fill Solution" button**. This will overwrite the `userInput` textarea to contain an example malicious payload that solves the challenge.
# Lodash
## Lodash special attributes
Many of Lodash's functions can be accessed in two different ways:
1. Accessing an attribute of the global `_` object and passing all inputs as arguments: `_.isEqual(1, 1)`
2. Passing an input as an argument to the `_` function to create a `LodashWrapper`, and calling an attribute of the wrapper with another input as argument: `_(1).isEqual(1)`
Instances of `LodashWrapper` store the wrapped inputs and other state data in various special attributes. Special attribute values that are already present in the original input to the `_` function are kept and used.
Note that Lodash comes in [different build varieties](https://github.com/lodash/lodash/wiki/build-differences) ("full", "core", "strict" and "custom" builds), which include different subsets of the functions available in the Lodash code base as well as differing in how some internals are implemented. The behavior of keeping special attribute values from input objects and many of the examples below will only work in the more commonly used ["full" build with usually 50M+ weekly downloads on npm](https://www.npmjs.com/package/lodash)
**LodashWrapper**
- `__wrapped__`: The wrapped value, i.e. the original input argument to the `_` function
- `__actions__`: An array of function references and arguments to be applied to the `__wrapped__` value when `.value()` is called
- `__chain__`: If this is set to `true`, any function call on the wrapper object other than `.value()` will push data to the `__actions__` array instead of executing immediately
- `__index__`: Keeps track of the current index when iterating via `.next()`
- `__values__`: Array used for iterating via `.next()`
- `__dir__`: Integer (-1|1) defines the direction from which to apply the `.drop()` and `.take()` methods
**LazyWrapper**
Extension of `LodashWrapper` which can defer more array operations
- `__filtered__`: Boolean affecting how other attributes related to array operations are treated
- `__iteratees__`: List of function references buffering `.filter()`, `.map()` and `.takeWhile()` operations
- `__takeCount__`: Integer affecting `.drop()` and `.take()` methods
- `__views__`: Alternative list of objects buffering `.drop()` and `.take()` operations
**Other**
- `length`: Standard JS attribute accessed by collection processing functions when the expected input is an Array-like object
- `__lodash_hash_undefined__`: Special value used to stand-in for `undefined` hash values
- `__lodash_placeholder__`: Special value used as an argument placeholder for function wrapping (e.g. via `_.bind`)
## Lodash DoS
**Abusing `length` property**
Several methods in the library assume a given value is an array and perform actions based on its `length` property, if the `length` value is a Number from `0` to `Number.MAX_SAFE_INTEGER`. The [upper limit](https://content.positive.security/code_playground/libs/lodash-4.17.21.js.html#islength) likely has its origin in [unrelated historical problems](https://underscorejs.org/docs/modules/_isArrayLike.html#section-2) (note that Lodash is originally a fork of Underscore). If these functions are used to process untrusted user input, crafted objects can be used instead of real arrays to cause very high memory and CPU consumption.
**Abusing `_.unset`/`_.omit`**
Unlike `_.set`, `_.unset` has not been patched to block access to prototype properties. The relevant code can also be reached when `_.omit` is called with a user controlled second parameter. If `_.unset` is called with a malicious path, functions and other properties can be deleted from the prototypes of JavaScript internals, potentially leading to crashes and DoS.
## Lodash control flow manipulation
When the `_()`/`lodash` function is used to wrap untrusted user input, a range of internal property names such as `__wrapped__`, `__actions__` or `__chain__` can be abused to craft objects that behave in drastically unexpected ways.
## Lodash RCE
Unlike `_.set`, `_.get` has not been patched to block access to prototype properties. Below is an example code snippet vulnerable to code execution. While this pattern does not seem too likely to appear organically, it still looks inconspicuous in a code review and could potentially be abused to insert backdoors into software.
## Lodash Bonus RCE
At the time of writing, the latest Lodash version `4.17.21` was pushed to npm more than four years ago. The lodash.com homepage still references an older version (`4.17.15`, published more than 6 years ago with 1.5m weekly npm downloads). Several prototype pollution vulnerabilities in Lodash have been published and fixed between these two releases. Below is an example showcasing how a very simple and inconspicuous code snippet can lead to code execution in `4.17.15`.
## How to prevent vulnerabilities in Lodash
- For any user-supplied non-Array Object passed into the `_` or `_.chain` functions, remove the following attributes: `__wrapped__`, `__actions__`, `__chain__`, `__index__`, `__values__`, `__dir__`, `__filtered__`, `__iteratees__`, `__takeCount__`, `__views__`, `length`
- Avoid passing user-supplied values as parameters that are treated as attribute paths (e.g. for `_.pick`, `_.get`, `_.set`, `_.unset`, etc.)
# Ramda
## Ramda special attributes
Ramda enables users to follow a more functional programming style and emphasizes the concept of [currying](https://fr.umio.us/favoring-curry/).
Ramda makes use of many different special attributes in its input parameters which can influence how data transformations are performed. Some of these input parameters with special attributes are themselves expected to be functions, which makes it unlikely that they are directly populated with user-supplied values. Others however are commonly filled with potentially user-supplied data such as strings, lists or objects.
**Automatic currying**
- `@@functional/placeholder`: When this property is set to `true` on a given function parameter, it is treated as an omitted parameter. The original function call will return a new function which takes the omitted parameter(s) as input to fully resolve the original data transformation. To illustrate, the following two snippets are equivalent: `R.add(1, 2);`, `R.add({"@@functional/placeholder": true}, 2)(1);`
**Transducer properties**
A transducer combines one or multiple transform functions (for example a filter function) with a reduce or aggregation function (such as a sum or averaging function, which condenses all given elements into a single accumulator value).
To allow transducer pipelines to be constructed from different functions or intermediate results which can also be used in other contexts, several special attributes are used:
- `@@transducer/init`: Called to obtain an accumulator value in case none is given for the reduce function
- `@@transducer/step`: Called with every element of the input iterable to update the accumulator value
- `@@transducer/result`: Called with the accumulator after all elements have been processed to obtain the final return value of the pipeline
- `@@transducer/reduced`: If this is set to `true` on an input parameter expected to be an iterable object, the `@@transducer/value` property will be returned instead of actually executing the reduce function
- `@@transducer/value`: Value returned in place of the reduce function's return value in case `@@transducer/reduced` is set to `true`
**Fantasy Land Specification**
The [Fantasy Land Specification](https://github.com/fantasyland/fantasy-land) aims to establish a common format for special attributes which contain functions. The goal is to make functions or intermediate results (algebraic structures) from different functional programming libraries interoperable.
Ramda implements a subset of this specification:
- `fantasy-land/ap`/`ap`: Applying a list of functions to the input data (Overriding `R.ap`)
- `fantasy-land/chain`: Concatenating output elements at the end of the chain operation (Overriding `R.chain`)
- `fantasy-land/concat`: Concatenation (Overriding `R.concat`)
- `fantasy-land/empty`/`empty`: Check if a value is empty (Overriding `R.empty`)
- `fantasy-land/eq`/`equals`: Equals check (Overriding `R.equals`)
- `fantasy-land/filter`: Filtering elements (Overriding `R.filter`)
- `fantasy-land/map`: Applying a function to the input data (Overriding `R.map`)
- `fantasy-land/of`/`of`: Instantiating a new object with a given type and value (Overriding `R.of`)
- `fantasy-land/promap`: Chaining multiple function calls (Overriding `R.promap`)
- `fantasy-land/reduce`/`reduce`: Aggregating multiple values into an accumulator (Overriding `R.reduce`)
- `fantasy-land/traverse`/`traverse`: Applying a function to the input data, then converting the resulting list to a function that operates on this list (Overriding `R.traverse`)
**Other**
- `clone`: Creating a copy of a value (Overriding `R.clone`)
- `indexOf`: Finding an element in a list (Overriding `R.indexOf`)
- `lastIndexOf`: Finding the last element in a list (Overriding `R.lastIndexOf`)
- `length`: Standard JS attribute accessed by collection processing functions when the expected input is an Array-like object
## Ramda DoS
Several methods in the library assume a given value is an array and perform actions based on its `.length` property. If these functions are used to process untrusted user input, crafted objects can be used instead of real arrays to cause very high memory and CPU consumption.
## Ramda control flow manipulation
In many scenarios Ramda does not enforce a strict separation of code and data by checking the processed data for special property names such as `fantasy-land/eq`, `@@transducer/reduced`, `@@transducer/value` and `@@functional/placeholder`.
## Ramda RCE
Below is a fictitious example code snippet vulnerable to code execution. The code pattern is quite convoluted and likely would never appear organically, but it could still look inconspicuous in a code review and potentially be abused to insert backdoors into software.
## How to prevent vulnerabilities in Ramda
- For any user-supplied non-Array Object passed to any Ramda function, remove the following attributes: `@@functional/placeholder`, `@@transducer/init`, `@@transducer/step`, `@@transducer/result`, `@@transducer/reduced`, `@@transducer/value`, `fantasy-land/ap`, `ap`, `fantasy-land/chain`, `fantasy-land/concat`, `fantasy-land/empty`, `empty`, `fantasy-land/equals`, `equals`, `fantasy-land/filter`, `fantasy-land/map`, `fantasy-land/of`, `of`, `fantasy-land/promap`, `fantasy-land/reduce`, `reduce`, `fantasy-land/traverse`, `traverse`, `clone`, `indexOf`, `lastIndexOf`, `length`
- Avoid passing user-supplied values as parameters that are treated as attribute paths (e.g. for `R.prop`, `R.path`, `R.pick`, `R.pluck`, etc.)
# Underscore
## Underscore special attributes
Underscore exposes much less attack surface compared to the other two:
- `length`: Standard JS attribute accessed by collection processing functions when the expected input is an Array-like object
- `_wrapped`: While Underscore also supports the concept of wrapped values via the `_()` function, it honors the special property `_wrapped` only after doing a proper type check on a given object, providing no clear path for exploitation
## Underscore DoS
Several methods in the library assume a given value is an array and perform actions based on its `length` property, if the `length` value is a Number from `0` to `Number.MAX_SAFE_INTEGER`. The [upper limit](https://content.positive.security/code_playground/libs/underscore-1.13.7.js.html#createSizePropertyCheck) likely has its origin in [unrelated historical problems](https://underscorejs.org/docs/modules/_isArrayLike.html#section-2). If these functions are used to process untrusted user input, crafted objects can be used instead of real arrays to cause very high memory and CPU consumption.
## How to prevent vulnerabilities in Underscore
- For any user-supplied non-Array Object passed to an Underscore Collection function, remove the `length` attribute ([The Underscore documentation also notes this](https://underscorejs.org/#collections))
- Avoid passing user-supplied values as parameters that are treated as attribute paths (e.g. for `_.get`, `_.property`, `_.pick`, etc.)
# Conclusion
**Hidden Complexity often leads to problems**
My personal favorite area to do vulnerability research in are hidden features and complexity in libraries or components that do many more things than expected by their typical user. Log4Shell and XXE are some prime examples for this concept.
I believe that many benefits could arise if more developers had the time and motivation to dig into the internals of the dependencies they are using.
We might see more contributions to the upstream open source projects, and more potential vulnerabilities might be caught before they can have widespread impact.
**Misalignment in expected library use cases should receive more attention**
In the case of Ramda and Underscore, maintainers have expressed that the codebases calling functions in their libraries should be responsible for the necessary sanitization.
This is in contrast to real world usage which includes many web application backend projects that feed untrusted inputs into the library and are potentially most affected by the issues described here.
**Finding the right balance between dependency minimalism and maximism benefits security**
A lot has already been written about software supply chain security and dependency bloat. Before including a new dependency into your codebase, we recommend doing a critical assessment on whether it is really required.
A few projects in the style of YouMightNotNeed{Library} have gained popularity recently. These are great resources to discover shorthand modern JS replacements for many of the simple utility library functions. Some of the implementations promoted there are however quite complex. If you choose to take these over instead, you should thoroughly review the code and be aware that package managers and dependency scanners will not be able to provide updates or alerts for outdated code copy-pasted into your codebase.
**Real world prevalance for this issue is hard to gauge**
We started going down this rabbit hole when we took a closer look at Ramda as part of a client engagement. Surprised by the simplicity of the DoS attack vector we decided to look at the other libraries as well.
It is quite interesting to see how similar the issues and examples between Lodash and Ramda are despite their totally different internal workings.
So far we have only found low and medium severity vulnerabilities in real closed source projects based on the issues described here.
We did not immediately find any exploitable open source applications in a rather superficial search on GitHub.
# Timeline
- `2024-09-27`: Reported Lodash findings via email (security@lodash.com)
- `2024-09-27`: Reported Underscore findings via email
- `2024-09-27`: Reported Ramda findings via email
- `2024-09-27`: Ramda maintainer responds confirming receipt of the report
- `2024-09-28`: Ramda maintainer creates GitHub security advisory drafts (not publicly accessible), proposing a "won't fix" resolution but inviting contributors for discussion
- `2024-09-29`: Underscore maintainer responds declaring the DoS does not warrant a fix
- `2024-10-06`: Lodash maintainer pushes some old commits and [updates the security policy to remove the old security@ email address](https://github.com/lodash/lodash/compare/6a2cc1dfcf7634fea70d1bc5bd22db453df67b42...afcd5bc1e8801867c31a17566e0e0edebb083d0e)
- `2024-10-08`: Emailed different Lodash maintainer forwarding the report and asking for an assessment
- `2024-10-29`: Privately messaged Lodash maintainer on Twitter/X asking for an acknowledgment of the report
- `2024-10-31`: Re-reported Lodash findings as GitHub security advisory drafts (not publicly accessible), in line with new security policy
- `2024-12-12`: Lodash security policy updates are [merged into the main branch](https://github.com/lodash/lodash/pull/5946) (still no acknowledgement of the reports)
-- MARKDOWN --