Skip to content

Commit 9837fb4

Browse files
Merge branch 'main' into cds-compile-retry
2 parents 2d14a8a + eaf18e2 commit 9837fb4

File tree

5 files changed

+341
-0
lines changed

5 files changed

+341
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Exported functions from CAP `cds.utils`.
3+
* Functions described from:
4+
* https://www.npmjs.com/package/@sap/cds?activeTab=code
5+
*/
6+
7+
import javascript
8+
import advanced_security.javascript.frameworks.cap.CDSUtils
9+
10+
abstract class UtilsAccessedPathSink extends DataFlow::Node { }
11+
12+
abstract class UtilsControlledDataSink extends DataFlow::Node { }
13+
14+
abstract class UtilsControlledPathSink extends DataFlow::Node { }
15+
16+
abstract class UtilsExtraFlow extends DataFlow::Node { }
17+
18+
/**
19+
* This represents the data in calls as follows:
20+
* ```javascript
21+
* await write ({foo:'bar'}) .to ('some','file.json')
22+
* ```
23+
* sinks in this example are:
24+
* ```javascript
25+
* {foo:'bar'}
26+
* ```
27+
*/
28+
class WrittenData extends UtilsControlledDataSink {
29+
WrittenData() { exists(FileWriters fw | fw.getData() = this) }
30+
}
31+
32+
/**
33+
* This represents the filepath accessed as an input for the data in calls as follows:
34+
* ```javascript
35+
* await copy('db/data').to('dist/db/data')
36+
* ```
37+
* sinks in this example are:
38+
* ```javascript
39+
* 'db/data'
40+
* ```
41+
*/
42+
class AccessedPath extends UtilsAccessedPathSink {
43+
AccessedPath() { exists(FileReaderWriters fw | fw.getFromPath() = this) }
44+
}
45+
46+
/**
47+
* This represents the filepath where data is written or a file operation is performed in calls as follows:
48+
* ```javascript
49+
* await write ({foo:'bar'}) .to ('some','file.json')
50+
* ```
51+
* sinks in this example are:
52+
* ```javascript
53+
* 'some'
54+
* 'file.json'
55+
* ```
56+
*/
57+
class ControlledInputPath extends UtilsControlledPathSink {
58+
ControlledInputPath() {
59+
exists(FileReaders fw | fw.getPath() = this)
60+
or
61+
exists(FileReaderWriters fw | fw.getToPath() = this)
62+
or
63+
exists(FileWriters fw | fw.getPath() = this)
64+
or
65+
exists(DirectoryWriters dw | dw.getPath() = this)
66+
or
67+
exists(DirectoryReaders dr | dr.getPath() = this)
68+
}
69+
}
70+
71+
/**
72+
* This represents calls where the taint flows through the call. e.g.
73+
* ```javascript
74+
* let dir = isdir ('app')
75+
* ```
76+
*/
77+
class AdditionalFlowStep extends UtilsExtraFlow {
78+
AdditionalFlowStep() {
79+
exists(PathConverters pc | pc.getPath() = this)
80+
or
81+
exists(PathPredicates pr | pr.getPath() = this)
82+
}
83+
84+
DataFlow::CallNode getOutgoingNode() { result = this }
85+
86+
DataFlow::Node getIngoingNode() { result = this.(DataFlow::CallNode).getAnArgument() }
87+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import javascript
2+
import advanced_security.javascript.frameworks.cap.CDS
3+
4+
/**
5+
* An access to the `utils` module on a CDS facade.
6+
*/
7+
class CdsUtilsModuleAccess extends API::Node {
8+
CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) }
9+
}
10+
11+
/**
12+
* CDS Utils:
13+
* `decodeURI`, `decodeURIComponent`, `local`
14+
*/
15+
class PathConverters extends DataFlow::CallNode {
16+
PathConverters() {
17+
exists(CdsUtilsModuleAccess utils |
18+
utils.getMember(["decodeURI", "decodeURIComponent", "local"]).getACall() = this
19+
)
20+
}
21+
22+
/**
23+
* Gets the arguments to these calls.
24+
*/
25+
DataFlow::Node getPath() { this.getAnArgument() = result }
26+
}
27+
28+
/**
29+
* CDS Utils:
30+
* `isdir`, `isfile`
31+
*/
32+
class PathPredicates extends DataFlow::CallNode {
33+
PathPredicates() {
34+
exists(CdsUtilsModuleAccess utils | utils.getMember(["isdir", "isfile"]).getACall() = this)
35+
}
36+
37+
/**
38+
* Gets the arguments to these calls.
39+
*/
40+
DataFlow::Node getPath() { this.getAnArgument() = result }
41+
}
42+
43+
/**
44+
* CDS Utils:
45+
* `find`, `stat`, `readdir`
46+
*/
47+
class DirectoryReaders extends DataFlow::CallNode {
48+
DirectoryReaders() {
49+
exists(CdsUtilsModuleAccess utils |
50+
utils.getMember(["find", "stat", "readdir"]).getACall() = this
51+
)
52+
}
53+
54+
/**
55+
* Gets the arguments to these calls.
56+
*/
57+
DataFlow::Node getPath() { this.getAnArgument() = result }
58+
}
59+
60+
/**
61+
* CDS Utils:
62+
* `mkdirp`, `rmdir`, `rimraf`, `rm`
63+
*/
64+
class DirectoryWriters extends DataFlow::CallNode {
65+
DirectoryWriters() {
66+
exists(CdsUtilsModuleAccess utils |
67+
utils.getMember(["mkdirp", "rmdir", "rimraf", "rm"]).getACall() = this
68+
)
69+
}
70+
71+
/**
72+
* Gets the arguments to these calls.
73+
*/
74+
DataFlow::Node getPath() { this.getAnArgument() = result }
75+
}
76+
77+
/**
78+
* CDS Utils:
79+
* `read`
80+
*/
81+
class FileReaders extends DataFlow::CallNode {
82+
FileReaders() { exists(CdsUtilsModuleAccess utils | utils.getMember(["read"]).getACall() = this) }
83+
84+
/**
85+
* Gets the 0th argument to these calls.
86+
*/
87+
DataFlow::Node getPath() { this.getArgument(0) = result }
88+
}
89+
90+
/**
91+
* CDS Utils:
92+
* `append`, `write`
93+
*/
94+
class FileWriters extends DataFlow::CallNode {
95+
FileWriters() {
96+
exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this)
97+
}
98+
99+
/**
100+
* Gets the arguments to these calls that represent data.
101+
*/
102+
DataFlow::Node getData() {
103+
this.getNumArgument() = 1 and
104+
this.getArgument(0) = result
105+
or
106+
this.getNumArgument() = 2 and
107+
this.getArgument(1) = result
108+
}
109+
110+
/**
111+
* Gets the arguments to these calls that represent a path.
112+
* Includes arguments to chained calls `to`, where that argument also represents a path.
113+
*/
114+
DataFlow::Node getPath() {
115+
this.getAMemberCall("to").getAnArgument() = result
116+
or
117+
this.getNumArgument() = 2 and
118+
this.getArgument(0) = result
119+
}
120+
}
121+
122+
/**
123+
* CDS Utils:
124+
* `copy`
125+
*/
126+
class FileReaderWriters extends DataFlow::CallNode {
127+
FileReaderWriters() {
128+
exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this)
129+
}
130+
131+
/**
132+
* Gets the arguments to these calls that represent a path from which data is read.
133+
*/
134+
DataFlow::Node getFromPath() { this.getArgument(0) = result }
135+
136+
/**
137+
* Gets the arguments to these calls that represent a path to which data is written.
138+
* Includes arguments to chained calls `to`, where that argument also represents a path.
139+
*/
140+
DataFlow::Node getToPath() {
141+
this.getAMemberCall("to").getArgument(_) = result
142+
or
143+
this.getArgument(1) = result
144+
}
145+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
| utils.js:5:21:5:30 | "%E0%A4%A" | "%E0%A4%A": additional flow step |
2+
| utils.js:7:31:7:40 | "%E0%A4%A" | "%E0%A4%A": additional flow step |
3+
| utils.js:9:18:9:27 | "%E0%A4%A" | "%E0%A4%A": additional flow step |
4+
| utils.js:13:17:13:21 | 'app' | 'app': additional flow step |
5+
| utils.js:15:19:15:32 | 'package.json' | 'package.json': additional flow step |
6+
| utils.js:17:22:17:35 | 'package.json' | 'package.json': controlled path sink |
7+
| utils.js:19:26:19:39 | 'package.json' | 'package.json': controlled path sink |
8+
| utils.js:21:20:21:33 | 'package.json' | 'package.json': controlled path sink |
9+
| utils.js:23:20:23:33 | 'package.json' | 'package.json': controlled path sink |
10+
| utils.js:25:14:25:22 | 'db/data' | 'db/data': controlled data sink |
11+
| utils.js:25:28:25:41 | 'dist/db/data' | 'dist/db/data': controlled path sink |
12+
| utils.js:26:14:26:22 | 'db/data' | 'db/data': controlled path sink |
13+
| utils.js:26:25:26:38 | 'dist/db/data' | 'dist/db/data': controlled data sink |
14+
| utils.js:28:12:28:20 | 'db/data' | 'db/data': accessed path sink |
15+
| utils.js:28:26:28:39 | 'dist/db/data' | 'dist/db/data': controlled path sink |
16+
| utils.js:29:12:29:20 | 'db/data' | 'db/data': accessed path sink |
17+
| utils.js:29:23:29:36 | 'dist/db/data' | 'dist/db/data': controlled path sink |
18+
| utils.js:31:13:31:26 | { foo: 'bar' } | { foo: 'bar' }: controlled data sink |
19+
| utils.js:31:32:31:47 | 'some/file.json' | 'some/file.json': controlled path sink |
20+
| utils.js:32:13:32:28 | 'some/file.json' | 'some/file.json': controlled path sink |
21+
| utils.js:32:31:32:44 | { foo: 'bar' } | { foo: 'bar' }: controlled data sink |
22+
| utils.js:34:14:34:19 | 'dist' | 'dist': controlled path sink |
23+
| utils.js:34:22:34:25 | 'db' | 'db': controlled path sink |
24+
| utils.js:34:28:34:33 | 'data' | 'data': controlled path sink |
25+
| utils.js:35:14:35:27 | 'dist/db/data' | 'dist/db/data': controlled path sink |
26+
| utils.js:37:13:37:18 | 'dist' | 'dist': controlled path sink |
27+
| utils.js:37:21:37:24 | 'db' | 'db': controlled path sink |
28+
| utils.js:37:27:37:32 | 'data' | 'data': controlled path sink |
29+
| utils.js:38:13:38:26 | 'dist/db/data' | 'dist/db/data': controlled path sink |
30+
| utils.js:40:14:40:19 | 'dist' | 'dist': controlled path sink |
31+
| utils.js:40:22:40:25 | 'db' | 'db': controlled path sink |
32+
| utils.js:40:28:40:33 | 'data' | 'data': controlled path sink |
33+
| utils.js:41:14:41:27 | 'dist/db/data' | 'dist/db/data': controlled path sink |
34+
| utils.js:43:10:43:15 | 'dist' | 'dist': controlled path sink |
35+
| utils.js:43:18:43:21 | 'db' | 'db': controlled path sink |
36+
| utils.js:43:24:43:29 | 'data' | 'data': controlled path sink |
37+
| utils.js:44:10:44:23 | 'dist/db/data' | 'dist/db/data': controlled path sink |
38+
| utils.js:52:20:52:28 | 'db/data' | 'db/data': controlled data sink |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const cds = require("@sap/cds");
2+
3+
const { decodeURI, decodeURIComponent, local, exists, isdir, isfile, read, readdir, append, write, copy, stat, find, mkdirp, rmdir, rimraf, rm } = cds.utils
4+
5+
let uri = decodeURI("%E0%A4%A") // taint step
6+
7+
let uri2 = decodeURIComponent("%E0%A4%A") // taint step
8+
9+
let uri3 = local("%E0%A4%A") // taint step
10+
11+
let uri4 = exists("%E0%A4%A") // NOT a taint step - returns a boolean
12+
13+
let dir = isdir('app') // taint step
14+
15+
let file = isfile('package.json') // taint step
16+
17+
let pkg = await read('package.json') // sink
18+
19+
let pdir = await readdir('package.json') // sink
20+
21+
let s = await stat('package.json') // sink
22+
23+
let f = await find('package.json') // sink
24+
25+
await append('db/data').to('dist/db/data') // sink
26+
await append('db/data', 'dist/db/data') // sink
27+
28+
await copy('db/data').to('dist/db/data') // sink
29+
await copy('db/data', 'dist/db/data') // sink
30+
31+
await write({ foo: 'bar' }).to('some/file.json') // sink
32+
await write('some/file.json', { foo: 'bar' }) // sink
33+
34+
await mkdirp('dist', 'db', 'data') // sink
35+
await mkdirp('dist/db/data') // sink
36+
37+
await rmdir('dist', 'db', 'data') // sink
38+
await rmdir('dist/db/data') // sink
39+
40+
await rimraf('dist', 'db', 'data') // sink
41+
await rimraf('dist/db/data') // sink
42+
43+
await rm('dist', 'db', 'data') // sink
44+
await rm('dist/db/data') // sink
45+
46+
function wrapperouter() {
47+
const temp = append
48+
wrapperinnermid(temp)
49+
}
50+
51+
function wrapperinnermid(temp) {
52+
const a = temp('db/data') // sink
53+
wrapperinner(a)
54+
}
55+
56+
function wrapperinner(a) {
57+
a.to('dist/db/data') // sink - [FALSE_NEGATIVE] - rare case as CAP is a fluent API
58+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import javascript
2+
import advanced_security.javascript.frameworks.cap.CAPPathInjectionQuery
3+
4+
from DataFlow::Node node, string str, string strfull
5+
where
6+
node.(UtilsControlledPathSink).toString() = str and strfull = str + ": controlled path sink"
7+
or
8+
node.(UtilsAccessedPathSink).toString() = str and strfull = str + ": accessed path sink"
9+
or
10+
node.(UtilsControlledDataSink).toString() = str and strfull = str + ": controlled data sink"
11+
or
12+
node.(UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step"
13+
select node, strfull

0 commit comments

Comments
 (0)