Skip to content

Commit c8af28c

Browse files
authored
Merge pull request github#13700 from asgerf/js/path-join-spread
JS: Recognize 'fs/promises' alias and handle spread arguments in path.join()
2 parents cffdc0a + 8234b8f commit c8af28c

File tree

7 files changed

+311
-19
lines changed

7 files changed

+311
-19
lines changed

javascript/ql/lib/semmle/javascript/frameworks/NodeJSLib.qll

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,11 @@ module NodeJSLib {
554554
t.start()
555555
or
556556
t.start() and
557-
result = DataFlow::moduleMember("fs", "promises")
557+
(
558+
result = DataFlow::moduleMember("fs", "promises")
559+
or
560+
result = DataFlow::moduleImport("fs/promises")
561+
)
558562
or
559563
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode pred | pred = fsModule(t2) |
560564
result = pred.track(t2, t)

javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,22 @@ module TaintedPath {
847847
dst = call and
848848
srclabel = dstlabel
849849
)
850+
or
851+
exists(DataFlow::CallNode join |
852+
// path.join() with spread argument
853+
join = NodeJSLib::Path::moduleMember("join").getACall() and
854+
src = join.getASpreadArgument() and
855+
dst = join and
856+
(
857+
srclabel.(Label::PosixPath).canContainDotDotSlash()
858+
or
859+
srclabel instanceof Label::SplitPath
860+
) and
861+
dstlabel.(Label::PosixPath).isNormalized() and
862+
if isRelative(join.getArgument(0).getStringValue())
863+
then dstlabel.(Label::PosixPath).isRelative()
864+
else dstlabel.(Label::PosixPath).isAbsolute()
865+
)
850866
}
851867

852868
/**
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The `fs/promises` package is now recognised as an alias for `require('fs').promises`.
5+
* The `js/path-injection` query can now track taint through calls to `path.join()` with a spread argument, such as `path.join(baseDir, ...args)`.

javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected

Lines changed: 247 additions & 0 deletions
Large diffs are not rendered by default.

javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/fs.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,12 @@ var fs = {};
4545
*/
4646
fs.readFileSync = function(filename, encoding) {};
4747

48+
/**
49+
* @param {string} filename
50+
* @param {string} encoding
51+
* @param {(function(NodeJS.ErrnoException, string): void)} callback
52+
* @return {void}
53+
*/
54+
fs.readFile = function(filename, encoding, callback) {};
55+
4856
module.exports = fs;

javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/normalizedPaths.js

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ app.get('/normalize-notAbsolute', (req, res) => {
3232

3333
if (pathModule.isAbsolute(path))
3434
return;
35-
35+
3636
fs.readFileSync(path); // NOT OK
3737

3838
if (!path.startsWith("."))
3939
fs.readFileSync(path); // OK
4040
else
4141
fs.readFileSync(path); // NOT OK - wrong polarity
42-
42+
4343
if (!path.startsWith(".."))
4444
fs.readFileSync(path); // OK
45-
45+
4646
if (!path.startsWith("../"))
4747
fs.readFileSync(path); // OK
4848

@@ -52,7 +52,7 @@ app.get('/normalize-notAbsolute', (req, res) => {
5252

5353
app.get('/normalize-noInitialDotDot', (req, res) => {
5454
let path = pathModule.normalize(req.query.path);
55-
55+
5656
if (path.startsWith(".."))
5757
return;
5858

@@ -80,7 +80,7 @@ app.get('/prepend-normalize', (req, res) => {
8080

8181
app.get('/absolute', (req, res) => {
8282
let path = req.query.path;
83-
83+
8484
if (!pathModule.isAbsolute(path))
8585
return;
8686

@@ -92,10 +92,10 @@ app.get('/absolute', (req, res) => {
9292

9393
app.get('/normalized-absolute', (req, res) => {
9494
let path = pathModule.normalize(req.query.path);
95-
95+
9696
if (!pathModule.isAbsolute(path))
9797
return;
98-
98+
9999
res.write(fs.readFileSync(path)); // NOT OK
100100

101101
if (path.startsWith('/home/user/www'))
@@ -104,7 +104,7 @@ app.get('/normalized-absolute', (req, res) => {
104104

105105
app.get('/combined-check', (req, res) => {
106106
let path = pathModule.normalize(req.query.path);
107-
107+
108108
// Combined absoluteness and folder check in one startsWith call
109109
if (path.startsWith("/home/user/www"))
110110
fs.readFileSync(path); // OK
@@ -121,7 +121,7 @@ app.get('/realpath', (req, res) => {
121121

122122
if (path.startsWith("/home/user/www"))
123123
fs.readFileSync(path); // OK - both absolute and normalized before check
124-
124+
125125
fs.readFileSync(pathModule.join('.', path)); // OK - normalized and coerced to relative
126126
fs.readFileSync(pathModule.join('/home/user/www', path)); // OK
127127
});
@@ -212,7 +212,7 @@ app.get('/join-regression', (req, res) => {
212212

213213
app.get('/decode-after-normalization', (req, res) => {
214214
let path = pathModule.normalize(req.query.path);
215-
215+
216216
if (!pathModule.isAbsolute(path) && !path.startsWith('..'))
217217
fs.readFileSync(path); // OK
218218

@@ -238,7 +238,7 @@ app.get('/resolve-path', (req, res) => {
238238
fs.readFileSync(path); // NOT OK
239239

240240
var self = something();
241-
241+
242242
if (path.substring(0, self.dir.length) === self.dir)
243243
fs.readFileSync(path); // OK
244244
else
@@ -256,12 +256,12 @@ app.get('/relative-startswith', (req, res) => {
256256
fs.readFileSync(path); // NOT OK
257257

258258
var self = something();
259-
259+
260260
var relative = pathModule.relative(self.webroot, path);
261261
if(relative.startsWith(".." + pathModule.sep) || relative == "..") {
262-
fs.readFileSync(path); // NOT OK!
262+
fs.readFileSync(path); // NOT OK!
263263
} else {
264-
fs.readFileSync(path); // OK!
264+
fs.readFileSync(path); // OK!
265265
}
266266

267267
let newpath = pathModule.normalize(path);
@@ -277,23 +277,23 @@ app.get('/relative-startswith', (req, res) => {
277277
if (relativePath.indexOf('../') === 0) {
278278
fs.readFileSync(newpath); // NOT OK!
279279
} else {
280-
fs.readFileSync(newpath); // OK!
280+
fs.readFileSync(newpath); // OK!
281281
}
282282

283283
let newpath = pathModule.normalize(path);
284284
var relativePath = pathModule.relative(pathModule.normalize(workspaceDir), newpath);
285285
if (pathModule.normalize(relativePath).indexOf('../') === 0) {
286286
fs.readFileSync(newpath); // NOT OK!
287287
} else {
288-
fs.readFileSync(newpath); // OK!
288+
fs.readFileSync(newpath); // OK!
289289
}
290290

291291
let newpath = pathModule.normalize(path);
292292
var relativePath = pathModule.relative(pathModule.normalize(workspaceDir), newpath);
293293
if (pathModule.normalize(relativePath).indexOf('../')) {
294294
fs.readFileSync(newpath); // OK!
295295
} else {
296-
fs.readFileSync(newpath); // NOT OK!
296+
fs.readFileSync(newpath); // NOT OK!
297297
}
298298
});
299299

@@ -340,7 +340,7 @@ app.get('/yet-another-prefix', (req, res) => {
340340

341341
fs.readFileSync(path); // NOT OK
342342

343-
var abs = pathModule.resolve(path);
343+
var abs = pathModule.resolve(path);
344344

345345
if (abs.indexOf(root) !== 0) {
346346
fs.readFileSync(path); // NOT OK
@@ -402,3 +402,8 @@ app.get('/dotdot-regexp', (req, res) => {
402402
fs.readFileSync(path); // OK
403403
}
404404
});
405+
406+
app.get('/join-spread', (req, res) => {
407+
fs.readFileSync(pathModule.join('foo', ...req.query.x.split('/'))); // NOT OK
408+
fs.readFileSync(pathModule.join(...req.query.x.split('/'))); // NOT OK
409+
});

javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/other-fs-libraries.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,10 @@ http.createServer(function(req, res) {
7171
mkdirp(path); // NOT OK
7272
mkdirp.sync(path); // NOT OK
7373
});
74+
75+
const fsp = require("fs/promises");
76+
http.createServer(function(req, res) {
77+
var path = url.parse(req.url, true).query.path;
78+
79+
fsp.readFile(path); // NOT OK
80+
});

0 commit comments

Comments
 (0)