Skip to content

Commit 9b10254

Browse files
committed
JS: support label-specific sanitizer guards
1 parent 5e72048 commit 9b10254

File tree

5 files changed

+81
-5
lines changed

5 files changed

+81
-5
lines changed

javascript/ql/src/semmle/javascript/dataflow/Configuration.qll

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ abstract class Configuration extends string {
146146
*/
147147
predicate isBarrier(DataFlow::Node node) {
148148
exists (BarrierGuardNode guard |
149+
guard.blocksAllLabels() and
149150
isBarrierGuard(guard) and
150151
guard.blocks(node)
151152
)
@@ -161,6 +162,17 @@ abstract class Configuration extends string {
161162
*/
162163
predicate isBarrier(DataFlow::Node src, DataFlow::Node trg, FlowLabel lbl) { none() }
163164

165+
/**
166+
* Holds if flow with label `lbl` cannot flow into `node`.
167+
*/
168+
predicate isBarrierForLabel(DataFlow::Node node, FlowLabel lbl) {
169+
exists (BarrierGuardNode guard |
170+
guard.blocksSpecificLabel(lbl) and
171+
isBarrierGuard(guard) and
172+
guard.blocks(node)
173+
)
174+
}
175+
164176
/**
165177
* Holds if data flow node `guard` can act as a barrier when appearing
166178
* in a condition.
@@ -298,6 +310,15 @@ abstract class BarrierGuardNode extends DataFlow::Node {
298310
*/
299311
abstract predicate blocks(boolean outcome, Expr e);
300312

313+
/**
314+
* Holds if this barrier guard blocks all labels.
315+
*/
316+
predicate blocksAllLabels() { any() }
317+
318+
/**
319+
* Holds if this barrier guard only blocks specific labels, and `label` is one of them.
320+
*/
321+
predicate blocksSpecificLabel(FlowLabel label) { none() }
301322
}
302323

303324
/**
@@ -570,7 +591,8 @@ private predicate flowThroughCall(DataFlow::Node input, DataFlow::Node invk,
570591
ret.asExpr() = f.getAReturnedExpr() and
571592
calls(invk, f) and // Do not consider partial calls
572593
reachableFromInput(f, invk, input, ret, cfg, summary) and
573-
not cfg.isBarrier(ret, invk)
594+
not cfg.isBarrier(ret, invk) and
595+
not cfg.isBarrierForLabel(invk, summary.getEndLabel())
574596
)
575597
}
576598

@@ -641,7 +663,8 @@ private predicate flowStep(DataFlow::Node pred, DataFlow::Configuration cfg,
641663
flowThroughProperty(pred, succ, cfg, summary)
642664
) and
643665
not cfg.isBarrier(succ) and
644-
not cfg.isBarrier(pred, succ)
666+
not cfg.isBarrier(pred, succ) and
667+
not cfg.isBarrierForLabel(succ, summary.getEndLabel())
645668
}
646669

647670
/**
@@ -666,6 +689,7 @@ private predicate reachableFromSource(DataFlow::Node nd, DataFlow::Configuration
666689
exists (FlowLabel lbl |
667690
isSource(nd, cfg, lbl) and
668691
not cfg.isBarrier(nd) and
692+
not cfg.isBarrierForLabel(nd, lbl) and
669693
summary = MkPathSummary(false, false, lbl, lbl)
670694
)
671695
or
@@ -684,7 +708,8 @@ private predicate onPath(DataFlow::Node nd, DataFlow::Configuration cfg,
684708
PathSummary summary) {
685709
reachableFromSource(nd, cfg, summary) and
686710
isSink(nd, cfg, summary.getEndLabel()) and
687-
not cfg.isBarrier(nd)
711+
not cfg.isBarrier(nd) and
712+
not cfg.isBarrierForLabel(nd, summary.getEndLabel())
688713
or
689714
exists (DataFlow::Node mid, PathSummary stepSummary |
690715
reachableFromSource(nd, cfg, summary) and

javascript/ql/src/semmle/javascript/security/TaintedObject.qll

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* 1. One or more sinks associated with the label `TaintedObject::label()`.
1111
* 2. The sources from `TaintedObject::isSource`.
1212
* 3. The flow steps from `TaintedObject::step`.
13+
* 4. The sanitizing guards `TaintedObject::SanitizerGuard`.
1314
*/
1415
import javascript
1516

@@ -80,5 +81,41 @@ module TaintedObject {
8081
}
8182
}
8283

83-
// TODO: string tests should be classified as sanitizer guards; need support for flow labels on guards
84+
/**
85+
* Sanitizer guard that blocks deep object taint.
86+
*/
87+
abstract class SanitizerGuard extends TaintTracking::SanitizerGuardNode {
88+
override predicate blocksAllLabels() { none() }
89+
90+
override predicate blocksSpecificLabel(FlowLabel label) {
91+
label = label()
92+
}
93+
}
94+
95+
/**
96+
* A test of form `typeof x === "something"`, preventing `x` from being an object in some cases.
97+
*/
98+
private class TypeTestGuard extends SanitizerGuard, ValueNode {
99+
override EqualityTest astNode;
100+
TypeofExpr typeof;
101+
boolean polarity;
102+
103+
TypeTestGuard() {
104+
astNode.getAnOperand() = typeof and
105+
(
106+
// typeof x === "object" sanitizes `x` when it evaluates to false
107+
astNode.getAnOperand().getStringValue() = "object" and
108+
polarity = astNode.getPolarity().booleanNot()
109+
or
110+
// typeof x === "string" sanitizes `x` when it evaluates to true
111+
astNode.getAnOperand().getStringValue() != "object" and
112+
polarity = astNode.getPolarity()
113+
)
114+
}
115+
116+
override predicate sanitizes(boolean outcome, Expr e) {
117+
polarity = outcome and
118+
e = typeof.getOperand()
119+
}
120+
}
84121
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ module NosqlInjection {
5454
node instanceof Sanitizer
5555
}
5656

57+
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
58+
guard instanceof TaintedObject::SanitizerGuard
59+
}
60+
5761
override predicate isAdditionalFlowStep(DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl) {
5862
TaintedObject::step(src, trg, inlbl, outlbl)
5963
or

javascript/ql/test/query-tests/Security/CWE-089/SqlInjection.expected

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
| mongodb.js:18:16:18:20 | query | This query depends on $@. | mongodb.js:13:19:13:26 | req.body | a user-provided value |
2-
| mongodb.js:45:16:45:20 | query | This query depends on $@. | mongodb.js:40:19:40:33 | req.query.title | a user-provided value |
2+
| mongodb.js:32:18:32:45 | { title ... itle) } | This query depends on $@. | mongodb.js:26:19:26:26 | req.body | a user-provided value |
3+
| mongodb.js:54:16:54:20 | query | This query depends on $@. | mongodb.js:49:19:49:33 | req.query.title | a user-provided value |
34
| mongodb_bodySafe.js:29:16:29:20 | query | This query depends on $@. | mongodb_bodySafe.js:24:19:24:33 | req.query.title | a user-provided value |
45
| mongoose.js:27:20:27:24 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |
56
| mongoose.js:30:25:30:29 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |

javascript/ql/test/query-tests/Security/CWE-089/mongodb.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ app.post('/documents/find', (req, res) => {
2222

2323
// OK: throws unless user-data is a string
2424
doc.find({ title: query.body.title.substr(1) });
25+
26+
let title = req.body.title;
27+
if (typeof title === "string") {
28+
// OK: input checked to be a string
29+
doc.find({ title: title });
30+
31+
// NOT OK: input is parsed as JSON after string check
32+
doc.find({ title: JSON.parse(title) });
33+
}
2534
});
2635
});
2736

0 commit comments

Comments
 (0)