Skip to content

Commit d147fab

Browse files
author
Max Schaefer
committed
Update qhelp for js/path-injection.
1 parent 8b6a918 commit d147fab

File tree

6 files changed

+252
-29
lines changed

6 files changed

+252
-29
lines changed

javascript/ql/src/Security/CWE-022/TaintedPath.qhelp

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,47 @@ attacker being able to influence behavior by modifying unexpected files.
1313

1414
<recommendation>
1515
<p>
16-
Validate user input before using it to construct a file path, either using an off-the-shelf library
17-
like the <code>sanitize-filename</code> npm package, or by performing custom validation.
16+
Validate user input before using it to construct a file path.
1817
</p>
1918

2019
<p>
21-
Ideally, follow these rules:
20+
The choice of validation depends on whether you want to allow the user to specify complex paths with multiple components that may span multiple folders, or only simple filenames without a path component.
2221
</p>
2322

24-
<ul>
25-
<li>Do not allow more than a single "." character.</li>
26-
<li>Do not allow directory separators such as "/" or "\" (depending on the file system).</li>
27-
<li>Do not rely on simply replacing problematic sequences such as "../". For example, after
28-
applying this filter to ".../...//", the resulting string would still be "../".</li>
29-
<li>Use a whitelist of known good patterns.</li>
30-
</ul>
23+
<p>
24+
In the former case, a common strategy is to make sure that the constructed file path is contained within a safe root folder.
25+
First, normalize the path using <code>path.resolve</code> or <code>fs.realpathSync</code> to remove any ".." segments.
26+
Then check that the normalized path starts with the root folder.
27+
Note that the normalization step is important, since otherwise even a path that starts with the root folder could be used to access files outside the root folder.
28+
</p>
29+
30+
<p>
31+
In the latter case, you can use a library like the <code>sanitize-filename</code> npm package to eliminate any special characters from the file path.
32+
Note that it is <i>not</i> sufficient to only remove "../" sequences: for example, applying this filter to ".../...//" would still result in the string "../".
33+
</p>
34+
35+
<p>
36+
Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that the user input matches one of these patterns.
37+
</p>
3138
</recommendation>
3239

3340
<example>
3441
<p>
35-
In the first example, a file name is read from an HTTP request and then used to access a file.
36-
However, a malicious user could enter a file name which is an absolute path, such as
37-
<code>"/etc/passwd"</code>.
42+
In the first example, a file name is read from an HTTP request and then used to access a file within a root folder.
43+
However, a malicious user could enter a file name containing "../" segments to navigate outside the root folder and access sensitive files.
3844
</p>
3945

46+
<sample src="examples/TaintedPath.js" />
47+
4048
<p>
41-
In the second example, it appears that the user is restricted to opening a file within the
42-
<code>"user"</code> home directory. However, a malicious user could enter a file name containing
43-
special characters. For example, the string <code>"../../etc/passwd"</code> will result in the code
44-
reading the file located at <code>"/home/user/../../etc/passwd"</code>, which is the system's
45-
password file. This file would then be sent back to the user, giving them access to all the
46-
system's passwords.
49+
The second example shows how to fix this.
50+
First, the file name is resolved relative to a root folder, which has the side effect of normalizing the path and removing any "../" segments.
51+
Then, <code>fs.realpathSync</code> is used to resolve any symbolic links in the path.
52+
Finally, we check that the normalized path starts with the root folder's path, which ensures that the file is contained within the root folder.
4753
</p>
4854

49-
<sample src="examples/TaintedPath.js" />
55+
<sample src="examples/TaintedPathGood.js" />
56+
5057
</example>
5158

5259
<references>
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
var fs = require('fs'),
2-
http = require('http'),
3-
url = require('url');
1+
const fs = require('fs'),
2+
http = require('http'),
3+
url = require('url');
4+
5+
const ROOT = "/var/www/";
46

57
var server = http.createServer(function(req, res) {
6-
let path = url.parse(req.url, true).query.path;
8+
let filePath = url.parse(req.url, true).query.path;
79

810
// BAD: This could read any file on the file system
9-
res.write(fs.readFileSync(path));
10-
11-
// BAD: This could still read any file on the file system
12-
res.write(fs.readFileSync("/home/user/" + path));
13-
});
11+
res.write(fs.readFileSync(ROOT + filePath, 'utf8'));
12+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const fs = require('fs'),
2+
http = require('http'),
3+
path = require('path'),
4+
url = require('url');
5+
6+
const ROOT = "/var/www/";
7+
8+
var server = http.createServer(function(req, res) {
9+
let filePath = url.parse(req.url, true).query.path;
10+
11+
// GOOD: Verify that the file path is under the root directory
12+
filePath = fs.realpathSync(path.resolve(ROOT, filePath));
13+
if (!filePath.startsWith(ROOT)) {
14+
res.statusCode = 403;
15+
res.end();
16+
return;
17+
}
18+
res.write(fs.readFileSync(filePath, 'utf8'));
19+
});

0 commit comments

Comments
 (0)