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;

if (
locales[name] === undefined &&
typeof module !== ‘undefined’ &&
module &&
module.exports
)
try
oldLocale = globalLocale._abbr;
aliasedRequire = require;
aliasedRequire(‘./locale/’ + name);
getSetGlobalLocale(oldLocale);
catch (e)

locales[name] = null;

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.

const aliasedRequire = require;

aliasedRequire(‘./locale/’ + name);

If we control the name parameter we could possibly pass a traversal based string:

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) => ‘en’;

const currentTime = moment().locale(locale).format(‘LLLL’);

res.send(`Current time ($locale): $currentTime`);
);

app.listen(port, () =>
console.log(`Server is running on http://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)

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!
Twitter/X
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!
Twitter/X