Skip to content

Commit ad645d3

Browse files
committed
JS: Restrict sendfile sink
1 parent 82ca45f commit ad645d3

File tree

4 files changed

+49
-10
lines changed

4 files changed

+49
-10
lines changed

javascript/ql/src/semmle/javascript/Concepts.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ abstract class FileSystemAccess extends DataFlow::Node {
3636
* sanitization to prevent the path arguments from traversing outside the root folder.
3737
*/
3838
DataFlow::Node getRootPathArgument() { none() }
39+
40+
/**
41+
* Holds if this file system access will reject paths containing path traversal
42+
* segments (`../`).
43+
*
44+
* `argument` should refer to the relevant path argument or root path argument.
45+
*/
46+
predicate isPathTraversalRejected(DataFlow::Node argument) { none() }
3947
}
4048

4149
/**

javascript/ql/src/semmle/javascript/frameworks/Express.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,10 @@ module Express {
839839
override DataFlow::Node getRootPathArgument() {
840840
result = this.(DataFlow::CallNode).getOptionArgument(1, "root")
841841
}
842+
843+
override predicate isPathTraversalRejected(DataFlow::Node argument) {
844+
argument = getAPathArgument()
845+
}
842846
}
843847

844848
/**

javascript/ql/src/semmle/javascript/security/dataflow/TaintedPath.qll

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ module TaintedPath {
1919
Configuration() { this = "TaintedPath" }
2020

2121
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
22-
source instanceof Source and
23-
label instanceof Label::PosixPath
22+
label = source.(Source).getAFlowLabel()
2423
}
2524

2625
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
27-
sink instanceof Sink and
28-
label instanceof Label::PosixPath
26+
label = sink.(Sink).getAFlowLabel()
2927
}
3028

3129
override predicate isBarrier(DataFlow::Node node) {

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ module TaintedPath {
1010
/**
1111
* A data flow source for tainted-path vulnerabilities.
1212
*/
13-
abstract class Source extends DataFlow::Node { }
13+
abstract class Source extends DataFlow::Node {
14+
/** Gets a flow label denoting the type of value for which this is a source. */
15+
DataFlow::FlowLabel getAFlowLabel() {
16+
result instanceof Label::PosixPath
17+
}
18+
}
1419

1520
/**
1621
* A data flow sink for tainted-path vulnerabilities.
1722
*/
18-
abstract class Sink extends DataFlow::Node { }
23+
abstract class Sink extends DataFlow::Node {
24+
/** Gets a flow label denoting the type of value for which this is a sink. */
25+
DataFlow::FlowLabel getAFlowLabel() {
26+
result instanceof Label::PosixPath
27+
}
28+
}
1929

2030
/**
2131
* A sanitizer for tainted-path vulnerabilities.
@@ -369,17 +379,36 @@ module TaintedPath {
369379
* A path argument to a file system access.
370380
*/
371381
class FsPathSink extends Sink, DataFlow::ValueNode {
382+
FileSystemAccess fileSystemAccess;
383+
372384
FsPathSink() {
373-
exists(FileSystemAccess fs |
374-
this = fs.getAPathArgument() and
375-
not exists(fs.getRootPathArgument())
385+
(
386+
this = fileSystemAccess.getAPathArgument() and
387+
not exists(fileSystemAccess.getRootPathArgument())
376388
or
377-
this = fs.getRootPathArgument()
389+
this = fileSystemAccess.getRootPathArgument()
378390
) and
379391
not this = any(ResolvingPathCall call).getInput()
380392
}
381393
}
382394

395+
/**
396+
* A path argument to a file system access, which disallows path traversal.
397+
*/
398+
private class FsPathSinkWithoutPathTraversal extends FsPathSink {
399+
FsPathSinkWithoutPathTraversal() {
400+
fileSystemAccess.isPathTraversalRejected(this)
401+
}
402+
403+
override DataFlow::FlowLabel getAFlowLabel() {
404+
// The protection is ineffective if the ../ segments have already
405+
// cancelled out against the intended root dir using path.join or similar.
406+
// Only flag normalized paths, as this corresponds to the output
407+
// of a normalizing call that had a malicious input.
408+
result.(Label::PosixPath).isNormalized()
409+
}
410+
}
411+
383412
/**
384413
* A path argument to the Express `res.render` method.
385414
*/

0 commit comments

Comments
 (0)