Skip to content

Commit 097b6e5

Browse files
authored
Merge pull request github#5794 from erik-krogh/rxPipe
Approved by asgerf
2 parents b1f28af + 902a436 commit 097b6e5

File tree

3 files changed

+76
-20
lines changed

3 files changed

+76
-20
lines changed

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

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ private class RxJsSubscribeStep extends TaintTracking::SharedTaintStep {
2323
* created by the `map` call.
2424
*/
2525
private DataFlow::Node pipeInput(DataFlow::CallNode pipe) {
26-
pipe = DataFlow::moduleMember("rxjs/operators", ["map", "filter"]).getACall() and
26+
pipe = DataFlow::moduleMember("rxjs/operators", any(string s | not s = "catchError")).getACall() and
2727
result = pipe.getCallback(0).getParameter(0)
2828
}
2929

@@ -34,7 +34,8 @@ private DataFlow::Node pipeInput(DataFlow::CallNode pipe) {
3434
* the pipe.
3535
*/
3636
private DataFlow::Node pipeOutput(DataFlow::CallNode pipe) {
37-
pipe = DataFlow::moduleMember("rxjs/operators", "map").getACall() and
37+
// we assume if there is a return, it is an output.
38+
pipe = DataFlow::moduleMember("rxjs/operators", _).getACall() and
3839
result = pipe.getCallback(0).getReturnNode()
3940
or
4041
pipe = DataFlow::moduleMember("rxjs/operators", "filter").getACall() and
@@ -48,30 +49,56 @@ private DataFlow::Node pipeOutput(DataFlow::CallNode pipe) {
4849
* be special-cased.
4950
*/
5051
private predicate isIdentityPipe(DataFlow::CallNode pipe) {
51-
pipe = DataFlow::moduleMember("rxjs/operators", "catchError").getACall()
52+
pipe = DataFlow::moduleMember("rxjs/operators", ["catchError", "tap"]).getACall()
53+
}
54+
55+
/**
56+
* A call to `pipe`, which is assumed to be an `rxjs/operators` pipe.
57+
*
58+
* Has utility methods `getInput`/`getOutput` to get the input/output of each
59+
* element of the pipe.
60+
* These utility methods automatically handle itentity pipes, and the
61+
* first/last elements of the pipe.
62+
*/
63+
private class RxJSPipe extends DataFlow::MethodCallNode {
64+
RxJSPipe() { this.getMethodName() = "pipe" }
65+
66+
/**
67+
* Gets an input to pipe element `i`.
68+
* Or if `i` is equal to the number of elements, gets the output of the pipe (the call itself)
69+
*/
70+
DataFlow::Node getInput(int i) {
71+
result = pipeInput(this.getArgument(i).getALocalSource())
72+
or
73+
i = this.getNumArgument() and
74+
result = this
75+
}
76+
77+
/**
78+
* Gets an output from pipe element `i`.
79+
* Handles identity pipes by getting the output from the previous element.
80+
* If `i` is -1, gets the receiver to the call, which started the pipe.
81+
*/
82+
DataFlow::Node getOutput(int i) {
83+
isIdentityPipe(this.getArgument(i).getALocalSource()) and
84+
result = getOutput(i - 1)
85+
or
86+
not isIdentityPipe(this.getArgument(i).getALocalSource()) and
87+
result = pipeOutput(this.getArgument(i).getALocalSource())
88+
or
89+
i = -1 and
90+
result = this.getReceiver()
91+
}
5292
}
5393

5494
/**
5595
* A step in or out of the map callback in a call of form `x.pipe(map(y => ...))`.
5696
*/
5797
private class RxJsPipeMapStep extends TaintTracking::SharedTaintStep {
5898
override predicate heapStep(DataFlow::Node pred, DataFlow::Node succ) {
59-
exists(DataFlow::MethodCallNode call | call.getMethodName() = "pipe" |
60-
pred = call.getReceiver() and
61-
succ = pipeInput(call.getArgument(0).getALocalSource())
62-
or
63-
exists(int i |
64-
pred = pipeOutput(call.getArgument(i).getALocalSource()) and
65-
succ = pipeInput(call.getArgument(i + 1).getALocalSource())
66-
)
67-
or
68-
pred = pipeOutput(call.getLastArgument().getALocalSource()) and
69-
succ = call
70-
or
71-
// Handle a common case where the last step is `catchError`.
72-
isIdentityPipe(call.getLastArgument().getALocalSource()) and
73-
pred = pipeOutput(call.getArgument(call.getNumArgument() - 2)) and
74-
succ = call
99+
exists(RxJSPipe pipe, int i |
100+
pred = pipe.getOutput(i) and
101+
succ = pipe.getInput(i + 1)
75102
)
76103
}
77104
}

javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ typeInferenceMismatch
109109
| promise.js:10:24:10:31 | source() | promise.js:10:8:10:32 | Promise ... urce()) |
110110
| promise.js:12:20:12:27 | source() | promise.js:13:8:13:23 | resolver.promise |
111111
| rxjs.js:3:1:3:8 | source() | rxjs.js:10:14:10:17 | data |
112+
| rxjs.js:13:1:13:8 | source() | rxjs.js:17:23:17:23 | x |
113+
| rxjs.js:13:1:13:8 | source() | rxjs.js:18:23:18:23 | x |
114+
| rxjs.js:13:1:13:8 | source() | rxjs.js:22:14:22:17 | data |
115+
| rxjs.js:27:24:27:32 | source(x) | rxjs.js:29:23:29:23 | x |
116+
| rxjs.js:27:24:27:32 | source(x) | rxjs.js:34:14:34:17 | data |
112117
| sanitizer-function.js:12:17:12:24 | source() | sanitizer-function.js:14:10:14:14 | taint |
113118
| sanitizer-function.js:12:17:12:24 | source() | sanitizer-function.js:33:14:33:18 | taint |
114119
| sanitizer-guards.js:2:11:2:18 | source() | sanitizer-guards.js:4:8:4:8 | x |

javascript/ql/test/library-tests/TaintTracking/rxjs.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { map, catchError } from 'rxjs/operators';
1+
import { map, tap, catchError, switchMap, filter } from 'rxjs/operators';
22

33
source()
44
.pipe(
@@ -9,3 +9,27 @@ source()
99
.subscribe(data => {
1010
sink(data)
1111
});
12+
13+
source()
14+
.pipe(
15+
map(x => x + 'foo'),
16+
// `tap` taps into the source observable, so like `subscribe` but inside the pipe.
17+
tap(x => sink(x)),
18+
tap(x => sink(x)),
19+
catchError(err => {})
20+
)
21+
.subscribe(data => {
22+
sink(data)
23+
});
24+
25+
myIdentifier()
26+
.pipe(
27+
switchMap(x => source(x)),
28+
filter(x => myFilter(x)),
29+
tap(x => sink(x)),
30+
catchError(err => {}),
31+
map(x => x + 'foo')
32+
)
33+
.subscribe(data => {
34+
sink(data)
35+
});

0 commit comments

Comments
 (0)