CVE-2022-24785
Description
Moment.js is a JavaScript date library for parsing, validating, manipulating, and formatting dates. A path traversal vulnerability impacts npm (server) users of Moment.js between versions 1.0.1
and 2.29.1
, especially if a user-provided locale string is directly used to switch moment locale. This problem is patched in 2.29.2
, and the patch can be applied to all affected versions. As a workaround, sanitize the user-provided locale name before passing it to Moment.js.
Identifying the vulnerability
We decided to download an affected version of MomentJS locally via npm.
Inside of Moment-JS/node_modules/moment/src/lib/locale/locales.js
there is a function named loadLocale
which takes the value of name
:
function loadLocale(name) {
var oldLocale = null,
aliasedRequire;
// TODO: Find a better way to register and load all the locales in Node
if (
locales[name] === undefined &&
typeof module !== 'undefined' &&
module &&
module.exports
) {
try {
oldLocale = globalLocale._abbr;
aliasedRequire = require;
aliasedRequire('./locale/' + name);
getSetGlobalLocale(oldLocale);
} catch (e) {
// mark as not found to avoid repeating expensive file require call causing high CPU
// when trying to find en-US, en_US, en-us for every format call
locales[name] = null; // null means not found
}
}
return locales[name];
}
The issue occurs on line 14
where the loadLocale()
function dynamically requires a module based on user input (name
). require()
is a Node.js function used to include and load modules or JavaScript files into a Node.js application.
//require set to a variable `aliasedRequire`
const aliasedRequire = require;
// essentially require('./locale/' + name);
aliasedRequire('./locale/' + name);
If we control the name
parameter we could possibly pass a traversal based string:
// Suppose name is set to a malicious value
const name = '../../someMaliciousModule';
aliasedRequire('./locale/' + name);
This could load ./locale/../../uploads/someMaliciousModule
, potentially exposing sensitive files, or even leading to Remote Code Execution (RCE).
Proof of concept
We wrote a basic application which uses the vulnerable function to demonstrate the vulnerability. Below is the app.js
code:
const express = require('express');
const moment = require('moment');
const app = express();
const port = 1337;
app.get('/time', (req, res) => {
const locale = req.query.locale || 'en';
// CVE-2022-24785 triggers at the following line in locate() function
// locale() function passes the first parameter to require() without any sanitisation, this makes it easier to perform a path traversal attack
const currentTime = moment().locale(locale).format('LLLL');
res.send(`Current time (${locale}): ${currentTime}`);
});
app.listen(port, () => {
console.log(`Server is running on https://localhost:${port}`);
});
The application listens on localhost:1337
and has an endpoint /time
that accepts a query parameter locale
. The application assigns the value of req.query.locale
to the variable locale
, defaulting to 'en'
if req.query.locale
is not provided. For example, if the query string is ?locale=fr
, then locale
would be 'fr'
. On the backend, the application dynamically loads a module corresponding to the specified locale using aliasedRequire();
However, passing ?locale=../../../../../../../etc/passwd
for example, does not work. When using require()
in Node.js, it attempts to load JavaScript modules or files. If we were to pass a value like ../../../../../../etc/passwd
to require('./locale/' + somevalue)
, Node.js would attempt to resolve this path relative to the current working directory of the application. However, Node.js does not directly read arbitrary files like /etc/passwd
through require()
because it expects modules or JavaScript files to load.
Now, let's assume the application has a file upload functionality which allows us to upload and store notes. We could use this to achieve RCE.
The path traversal combined with the ability to upload a file even .txt
or note
(no extension) provides us with RCE due to require();
.
Is the patch secure?
Let's review the patch code for the latest version of Moment JS.
npm install moment@latest
function isLocaleNameSane(name) {
// Prevent names that look like filesystem paths, i.e contain '/' or '\'
// Ensure name is available and function returns boolean
return !!(name && name.match('^[^/\\\\]*$'));
}
As of right now, there is no current way to bypass this regular expression. I've tried multiple techniques.
Credits
This discovery was a joint effort between me and my good friend Isira Adithya. Below will be Isiras Twitter/X, and his LinkedIn!
Well, that's all.
Essentially, that's all. It's a very basic vulnerability! As far as I am aware, no one has covered a proof of concept for this vulnerability, so this is pretty cool for everyone to see.
A Twitter/X follow is always appreciated!