Skip to content

Commit 7702b58

Browse files
author
Max Schaefer
authored
Merge pull request #305 from asger-semmle/json-taint-kind
JS: Add flow label for tainted objects and sharpen NosqlInjection
2 parents c78f3f8 + b72e2aa commit 7702b58

File tree

13 files changed

+323
-17
lines changed

13 files changed

+323
-17
lines changed

javascript/ql/src/Security/CWE-089/SqlInjection.actual

Whitespace-only changes.

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ abstract class Configuration extends string {
9393
}
9494

9595
/**
96-
* Holds if `source` is a source of flow labelled with `lbl` that is relevant
96+
* Holds if `source` is a source of flow labeled with `lbl` that is relevant
9797
* for this configuration.
9898
*/
9999
predicate isSource(DataFlow::Node source, FlowLabel lbl) {
@@ -108,7 +108,7 @@ abstract class Configuration extends string {
108108
}
109109

110110
/**
111-
* Holds if `sink` is a sink of flow labelled with `lbl` that is relevant
111+
* Holds if `sink` is a sink of flow labeled with `lbl` that is relevant
112112
* for this configuration.
113113
*/
114114
predicate isSink(DataFlow::Node sink, FlowLabel lbl) {
@@ -146,6 +146,7 @@ abstract class Configuration extends string {
146146
*/
147147
predicate isBarrier(DataFlow::Node node) {
148148
exists (BarrierGuardNode guard |
149+
not guard instanceof LabeledBarrierGuardNode 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 isLabeledBarrier(DataFlow::Node node, FlowLabel lbl) {
169+
exists (LabeledBarrierGuardNode guard |
170+
lbl = guard.getALabel() 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.
@@ -297,7 +309,16 @@ abstract class BarrierGuardNode extends DataFlow::Node {
297309
* Holds if this node blocks expression `e` provided it evaluates to `outcome`.
298310
*/
299311
abstract predicate blocks(boolean outcome, Expr e);
312+
}
300313

314+
/**
315+
* A guard node that only blocks specific labels.
316+
*/
317+
abstract class LabeledBarrierGuardNode extends BarrierGuardNode {
318+
/**
319+
* Get a flow label blocked by this guard node.
320+
*/
321+
abstract FlowLabel getALabel();
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.isLabeledBarrier(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.isLabeledBarrier(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.isLabeledBarrier(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.isLabeledBarrier(nd, summary.getEndLabel())
688713
or
689714
exists (DataFlow::Node mid, PathSummary stepSummary |
690715
reachableFromSource(nd, cfg, summary) and

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ module TaintTracking {
130130
* configurations it is used in.
131131
*
132132
* Note: For performance reasons, all subclasses of this class should be part
133-
* of the standard library. Override `Configuration::isTaintSanitizerGuard`
133+
* of the standard library. Override `Configuration::isSanitizer`
134134
* for analysis-specific taint steps.
135135
*/
136136
abstract class AdditionalSanitizerGuardNode extends SanitizerGuardNode {
@@ -159,6 +159,12 @@ module TaintTracking {
159159

160160
}
161161

162+
/**
163+
* A sanitizer guard node that only blocks specific flow labels.
164+
*/
165+
abstract class LabeledSanitizerGuardNode extends SanitizerGuardNode, DataFlow::LabeledBarrierGuardNode {
166+
}
167+
162168
/**
163169
* DEPRECATED: Override `Configuration::isAdditionalTaintStep` or use
164170
* `AdditionalTaintStep` instead.

javascript/ql/src/semmle/javascript/dataflow/internal/FlowSteps.qll

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ predicate localFlowStep(DataFlow::Node pred, DataFlow::Node succ,
7474
any(DataFlow::AdditionalFlowStep afs).step(pred, succ) and predlbl = succlbl
7575
or
7676
exists (boolean vp | configuration.isAdditionalFlowStep(pred, succ, vp) |
77-
if vp = false and (predlbl = FlowLabel::data() or predlbl = FlowLabel::taint()) then
78-
succlbl = FlowLabel::taint()
79-
else
80-
predlbl = succlbl
77+
vp = true and
78+
predlbl = succlbl
79+
or
80+
vp = false and
81+
(predlbl = FlowLabel::data() or predlbl = FlowLabel::taint()) and
82+
succlbl = FlowLabel::taint()
8183
)
8284
or
8385
configuration.isAdditionalFlowStep(pred, succ, predlbl, succlbl)

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,31 @@ module Express {
488488
override string getKind() {
489489
result = kind
490490
}
491+
492+
override predicate isUserControlledObject() {
493+
kind = "body" and
494+
exists (ExpressLibraries::BodyParser bodyParser, RouteHandlerExpr expr |
495+
expr.getBody() = rh and
496+
bodyParser.producesUserControlledObjects() and
497+
bodyParser.flowsToExpr(expr.getAMatchingAncestor())
498+
)
499+
or
500+
// If we can't find the middlewares for the route handler,
501+
// but all known body parsers are deep, assume req.body is a deep object.
502+
kind = "body" and
503+
forall(ExpressLibraries::BodyParser bodyParser | bodyParser.producesUserControlledObjects())
504+
or
505+
kind = "parameter" and
506+
exists (DataFlow::Node request | request = DataFlow::valueNode(rh.getARequestExpr()) |
507+
this.(DataFlow::MethodCallNode).calls(request, "param")
508+
or
509+
exists (DataFlow::PropRead base |
510+
// `req.query.name`
511+
base.accesses(request, "query") and
512+
this = base.getAPropertyReference(_)
513+
)
514+
)
515+
}
491516
}
492517

493518
/**

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,53 @@ module ExpressLibraries {
230230

231231
}
232232

233+
/**
234+
* An instance of the Express `body-parser` middleware.
235+
*/
236+
class BodyParser extends DataFlow::InvokeNode {
237+
string kind;
238+
239+
BodyParser() {
240+
this = DataFlow::moduleImport("body-parser").getACall() and kind = "json"
241+
or
242+
exists (string moduleName |
243+
(moduleName = "body-parser" or moduleName = "express") and
244+
(kind = "json" or kind = "urlencoded") and
245+
this = DataFlow::moduleMember(moduleName, kind).getACall()
246+
)
247+
}
248+
249+
/**
250+
* Holds if this is a JSON body parser.
251+
*/
252+
predicate isJson() {
253+
kind = "json"
254+
}
255+
256+
/**
257+
* Holds if this is a URL-encoded body parser.
258+
*/
259+
predicate isUrlEncoded() {
260+
kind = "urlencoded"
261+
}
262+
263+
/**
264+
* Holds if this is an extended URL-encoded body parser.
265+
*
266+
* The extended URL-encoding allows for nested objects, like JSON.
267+
*/
268+
predicate isExtendedUrlEncoded() {
269+
kind = "urlencoded" and
270+
not getOptionArgument(0, "extended").mayHaveBooleanValue(false)
271+
}
272+
273+
/**
274+
* Holds if this parses the input as JSON or extended URL-encoding, resulting
275+
* in user-controlled objects (as opposed to user-controlled strings).
276+
*/
277+
predicate producesUserControlledObjects() {
278+
isJson() or isExtendedUrlEncoded()
279+
}
280+
}
281+
233282
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Provides methods for reasoning about the flow of deeply tainted objects, such as JSON objects
3+
* parsed from user-controlled data.
4+
*
5+
* Deeply tainted objects are arrays or objects with user-controlled property names, containing
6+
* tainted values or deeply tainted objects in their properties.
7+
*
8+
* To track deeply tainted objects, a flow-tracking configuration should generally include the following:
9+
*
10+
* 1. One or more sinks associated with the label `TaintedObject::label()`.
11+
* 2. The sources from `TaintedObject::isSource`.
12+
* 3. The flow steps from `TaintedObject::step`.
13+
* 4. The sanitizing guards `TaintedObject::SanitizerGuard`.
14+
*/
15+
import javascript
16+
17+
module TaintedObject {
18+
private import DataFlow
19+
20+
private class TaintedObjectLabel extends FlowLabel {
21+
TaintedObjectLabel() { this = "tainted-object" }
22+
}
23+
24+
/**
25+
* Gets the flow label representing a deeply tainted object.
26+
*
27+
* A "tainted object" is an array or object whose property values are all assumed to be tainted as well.
28+
*
29+
* Note that the presence of the this label generally implies the presence of the `taint` label as well.
30+
*/
31+
FlowLabel label() { result instanceof TaintedObjectLabel }
32+
33+
/**
34+
* Holds for the flows steps that are relevant for tracking user-controlled JSON objects.
35+
*/
36+
predicate step(Node src, Node trg, FlowLabel inlbl, FlowLabel outlbl) {
37+
// JSON parsers map tainted inputs to tainted JSON
38+
(inlbl = FlowLabel::data() or inlbl = FlowLabel::taint()) and
39+
outlbl = label() and
40+
exists (JsonParserCall parse |
41+
src = parse.getInput() and
42+
trg = parse.getOutput())
43+
or
44+
// Property reads preserve deep object taint.
45+
inlbl = label() and
46+
outlbl = label() and
47+
trg.(PropRead).getBase() = src
48+
or
49+
// Property projection preserves deep object taint
50+
inlbl = label() and
51+
outlbl = label() and
52+
trg.(PropertyProjection).getObject() = src
53+
or
54+
// Extending objects preserves deep object taint
55+
inlbl = label() and
56+
outlbl = label() and
57+
exists (ExtendCall call |
58+
src = call.getAnOperand() and
59+
trg = call
60+
or
61+
src = call.getASourceOperand() and
62+
trg = call.getDestinationOperand().getALocalSource())
63+
}
64+
65+
/**
66+
* Holds if `node` is a source of JSON taint and label is the JSON taint label.
67+
*/
68+
predicate isSource(Node source, FlowLabel label) {
69+
source instanceof Source and label = label()
70+
}
71+
72+
/**
73+
* A source of a user-controlled deep object.
74+
*/
75+
abstract class Source extends DataFlow::Node {}
76+
77+
/** Request input accesses as a JSON source. */
78+
private class RequestInputAsSource extends Source {
79+
RequestInputAsSource() {
80+
this.(HTTP::RequestInputAccess).isUserControlledObject()
81+
}
82+
}
83+
84+
/**
85+
* Sanitizer guard that blocks deep object taint.
86+
*/
87+
abstract class SanitizerGuard extends TaintTracking::LabeledSanitizerGuardNode {
88+
override FlowLabel getALabel() {
89+
result = label()
90+
}
91+
}
92+
93+
/**
94+
* A test of form `typeof x === "something"`, preventing `x` from being an object in some cases.
95+
*/
96+
private class TypeTestGuard extends SanitizerGuard, ValueNode {
97+
override EqualityTest astNode;
98+
TypeofExpr typeof;
99+
boolean polarity;
100+
101+
TypeTestGuard() {
102+
astNode.getAnOperand() = typeof and
103+
(
104+
// typeof x === "object" sanitizes `x` when it evaluates to false
105+
astNode.getAnOperand().getStringValue() = "object" and
106+
polarity = astNode.getPolarity().booleanNot()
107+
or
108+
// typeof x === "string" sanitizes `x` when it evaluates to true
109+
astNode.getAnOperand().getStringValue() != "object" and
110+
polarity = astNode.getPolarity()
111+
)
112+
}
113+
114+
override predicate sanitizes(boolean outcome, Expr e) {
115+
polarity = outcome and
116+
e = typeof.getOperand()
117+
}
118+
}
119+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ module ClientSideUrlRedirect {
6565
queryAccess(pred, succ) and
6666
f instanceof DocumentUrl and
6767
g = DataFlow::FlowLabel::taint()
68+
or
69+
// preserve document.url label in step from `location` to `location.href`
70+
f instanceof DocumentUrl and
71+
g instanceof DocumentUrl and
72+
succ.(DataFlow::PropRead).accesses(pred, "href")
6873
}
6974
}
7075

0 commit comments

Comments
 (0)