Skip to content

Commit 0f85a52

Browse files
authored
Merge pull request #7773 from erik-krogh/CWE-367
JS: add a js/file-system-race query
2 parents 3597d80 + a51f892 commit 0f85a52

File tree

8 files changed

+235
-0
lines changed

8 files changed

+235
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Often it is necessary to check the state of a file before using it. These checks usually take a file name
9+
to be checked, and if the check returns positively, then the file is opened or otherwise operated upon.
10+
</p>
11+
12+
<p>
13+
However, in the time between the check and the operation, the underlying file referenced by the
14+
file name could be changed by an attacker, causing unexpected behavior.
15+
</p>
16+
17+
</overview>
18+
19+
<recommendation>
20+
<p>
21+
Use file descriptors instead of file names whenever possible.
22+
</p>
23+
</recommendation>
24+
25+
<example>
26+
<p>
27+
The following example shows a case where the code checks whether a file inside the <code>/tmp/</code> folder exists,
28+
and if it doesn't, the file is written to that location.
29+
</p>
30+
31+
<sample src="examples/file-race-bad.js" />
32+
33+
<p>
34+
However, in a multi-user environment the file might be created by another user between the existence check and the write.
35+
</p>
36+
37+
<p>
38+
This can be avoided by using <code>fs.open</code> to get a file descriptor, and then use that file descriptor in the write operation.
39+
</p>
40+
41+
<sample src="examples/file-race-good.js" />
42+
43+
</example>
44+
45+
<references>
46+
<li>
47+
Wikipedia: <a href="https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use">Time-of-check to time-of-use</a>.
48+
</li>
49+
50+
<li>
51+
The CERT Oracle Secure Coding Standard for C:
52+
<a href="https://www.securecoding.cert.org/confluence/display/c/FIO01-C.+Be+careful+using+functions+that+use+file+names+for+identification">
53+
FIO01-C. Be careful using functions that use file names for identification
54+
</a>.
55+
</li>
56+
<li>
57+
NodeJS:
58+
<a href="https://nodejs.org/api/fs.html">The FS module</a>.
59+
</li>
60+
</references>
61+
</qhelp>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* @name Potential file system race condition
3+
* @description Separately checking the state of a file before operating
4+
* on it may allow an attacker to modify the file between
5+
* the two operations.
6+
* @kind problem
7+
* @problem.severity warning
8+
* @security-severity 7.7
9+
* @precision medium
10+
* @id js/file-system-race
11+
* @tags security
12+
* external/cwe/cwe-367
13+
*/
14+
15+
import javascript
16+
17+
/**
18+
* A call that checks a property of some file.
19+
*/
20+
class FileCheck extends DataFlow::CallNode {
21+
FileCheck() {
22+
this =
23+
NodeJSLib::FS::moduleMember([
24+
"open", "openSync", "exists", "existsSync", "stat", "statSync", "lstat", "lstatSync",
25+
"fstat", "fstatSync", "access", "accessSync"
26+
]).getACall()
27+
}
28+
29+
DataFlow::Node getPathArgument() { result = this.getArgument(0) }
30+
}
31+
32+
/**
33+
* A call that modifies or otherwise interacts with a file.
34+
*/
35+
class FileUse extends DataFlow::CallNode {
36+
FileUse() {
37+
this =
38+
NodeJSLib::FS::moduleMember([
39+
// these are the six methods that accept file paths and file descriptors
40+
"readFile", "readFileSync", "writeFile", "writeFileSync", "appendFile", "appendFileSync",
41+
// don't use "open" after e.g. "access"
42+
"open", "openSync"
43+
]).getACall()
44+
}
45+
46+
DataFlow::Node getPathArgument() { result = this.getArgument(0) }
47+
}
48+
49+
/**
50+
* Gets a reference to a file-handle from a call to `open` or `openSync`.
51+
*/
52+
DataFlow::SourceNode getAFileHandle(DataFlow::TypeTracker t) {
53+
t.start() and
54+
(
55+
result = NodeJSLib::FS::moduleMember("openSync").getACall()
56+
or
57+
result =
58+
NodeJSLib::FS::moduleMember("open")
59+
.getACall()
60+
.getLastArgument()
61+
.getAFunctionValue()
62+
.getParameter(1)
63+
)
64+
or
65+
exists(DataFlow::TypeTracker t2 | result = getAFileHandle(t2).track(t2, t))
66+
}
67+
68+
/**
69+
* Holds if `check` and `use` operate on the same file.
70+
*/
71+
predicate checkAndUseOnSame(FileCheck check, FileUse use) {
72+
exists(string path |
73+
check.getPathArgument().mayHaveStringValue(path) and
74+
use.getPathArgument().mayHaveStringValue(path)
75+
)
76+
or
77+
AccessPath::getAnAliasedSourceNode(check.getPathArgument()).flowsTo(use.getPathArgument())
78+
}
79+
80+
/**
81+
* Holds if `check` happens before `use`.
82+
*/
83+
pragma[inline]
84+
predicate useAfterCheck(FileCheck check, FileUse use) {
85+
check.getCallback(_).getFunction() = use.getContainer()
86+
or
87+
exists(BasicBlock bb |
88+
check.getBasicBlock() = bb and
89+
use.getBasicBlock() = bb and
90+
exists(int i, int j |
91+
bb.getNode(i) = check.asExpr() and
92+
bb.getNode(j) = use.asExpr() and
93+
i < j
94+
)
95+
)
96+
or
97+
check.getBasicBlock().getASuccessor+() = use.getBasicBlock()
98+
}
99+
100+
from FileCheck check, FileUse use
101+
where
102+
checkAndUseOnSame(check, use) and
103+
useAfterCheck(check, use) and
104+
not getAFileHandle(DataFlow::TypeTracker::end()).flowsTo(use.getPathArgument())
105+
select use, "The file may have changed since it $@.", check, "was checked"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const fs = require("fs");
2+
const os = require("os");
3+
const path = require("path");
4+
5+
const filePath = path.join(os.tmpdir(), "my-temp-file.txt");
6+
7+
if (!fs.existsSync(filePath)) {
8+
fs.writeFileSync(filePath, "Hello", { mode: 0o600 });
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const fs = require("fs");
2+
const os = require("os");
3+
const path = require("path");
4+
5+
const filePath = path.join(os.tmpdir(), "my-temp-file.txt");
6+
7+
try {
8+
const fd = fs.openSync(filePath, fs.O_CREAT | fs.O_EXCL | fs.O_RDWR, 0o600);
9+
10+
fs.writeFileSync(fd, "Hello");
11+
} catch (e) {
12+
// file existed
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* A new query `js/file-system-race` has been added. The query detects when there is time between a file being checked and used. The query is not run by default.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| tst.js:8:3:8:54 | fs.writ ... o600 }) | The file may have changed since it $@. | tst.js:7:6:7:28 | fs.exis ... lePath) | was checked |
2+
| tst.js:14:3:14:40 | fs.writ ... ntent") | The file may have changed since it $@. | tst.js:12:15:12:36 | fs.stat ... ePath2) | was checked |
3+
| tst.js:18:3:18:40 | fs.writ ... ntent") | The file may have changed since it $@. | tst.js:17:1:19:2 | fs.acce ... T OK\\n}) | was checked |
4+
| tst.js:33:3:37:4 | fs.open ... ..\\n }) | The file may have changed since it $@. | tst.js:27:1:38:2 | fs.acce ... });\\n}) | was checked |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-367/FileSystemRace.ql
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const fs = require("fs");
2+
const os = require("os");
3+
const path = require("path");
4+
5+
const filePath = path.join(os.tmpdir(), "my-temp-file.txt");
6+
7+
if (!fs.existsSync(filePath)) {
8+
fs.writeFileSync(filePath, "Hello", { mode: 0o600 }); // NOT OK
9+
}
10+
11+
const filePath2 = createFile();
12+
const stats = fs.statSync(filePath2);
13+
if (doSomethingWith(stats)) {
14+
fs.writeFileSync(filePath2, "content"); // NOT OK
15+
}
16+
17+
fs.access(filePath2, fs.constants.F_OK, (err) => {
18+
fs.writeFileSync(filePath2, "content"); // NOT OK
19+
});
20+
21+
fs.open("myFile", "rw", (err, fd) => {
22+
fs.writeFileSync(fd, "content"); // OK
23+
});
24+
25+
import { open, close } from "fs";
26+
27+
fs.access("myfile", (err) => {
28+
if (!err) {
29+
console.error("myfile already exists");
30+
return;
31+
}
32+
33+
fs.open("myfile", "wx", (err, fd) => { // NOT OK
34+
if (err) throw err;
35+
36+
// ....
37+
});
38+
});

0 commit comments

Comments
 (0)