Skip to content

Commit 2e57a7d

Browse files
committed
JS: Add ClientSideRemoteFlowSource
1 parent aa360c0 commit 2e57a7d

26 files changed

+653
-787
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,35 @@ module TaintTracking {
239239
}
240240
}
241241

242+
/** Gets a data flow node referring to the client side URL. */
243+
private DataFlow::SourceNode clientSideUrlRef(DataFlow::TypeTracker t) {
244+
t.start() and
245+
result.(ClientSideRemoteFlowSource).getKind().isUrl()
246+
or
247+
exists(DataFlow::TypeTracker t2 | result = clientSideUrlRef(t2).track(t2, t))
248+
}
249+
250+
/** Gets a data flow node referring to the client side URL. */
251+
private DataFlow::SourceNode clientSideUrlRef() {
252+
result = clientSideUrlRef(DataFlow::TypeTracker::end())
253+
}
254+
255+
/**
256+
* Holds if `read` reads a property of the client-side URL, which is not tainted.
257+
* In this case, the read is excluded from the default set of taint steps.
258+
*/
259+
private predicate isSafeClientSideUrlProperty(DataFlow::PropRead read) {
260+
// Block all properties of client-side URLs, as .hash and .search are considered sources of their own
261+
read = clientSideUrlRef().getAPropertyRead()
262+
or
263+
exists(StringSplitCall c |
264+
c.getBaseString().getALocalSource() =
265+
[DOM::locationRef(), DOM::locationRef().getAPropertyRead("href")] and
266+
c.getSeparator() = "?" and
267+
read = c.getAPropertyRead("0")
268+
)
269+
}
270+
242271
/**
243272
* Holds if there is taint propagation through the heap from `pred` to `succ`.
244273
*/
@@ -264,7 +293,8 @@ module TaintTracking {
264293
or
265294
// reading from a tainted object yields a tainted result
266295
succ.(DataFlow::PropRead).getBase() = pred and
267-
not AccessPath::DominatingPaths::hasDominatingWrite(succ)
296+
not AccessPath::DominatingPaths::hasDominatingWrite(succ) and
297+
not isSafeClientSideUrlProperty(succ)
268298
or
269299
// iterating over a tainted iterator taints the loop variable
270300
exists(ForOfStmt fos |

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,46 +77,61 @@ module Angular2 {
7777
}
7878

7979
/** Gets a reference to a `ParamMap` object, usually containing values from the URL. */
80-
DataFlow::SourceNode paramMap() {
81-
result.hasUnderlyingType("@angular/router", "ParamMap")
80+
private DataFlow::SourceNode paramMap(ClientSideRemoteFlowKind kind) {
81+
result.hasUnderlyingType("@angular/router", "ParamMap") and kind.isPath()
8282
or
83-
result = activatedRouteProp(["paramMap", "queryParamMap"])
83+
result = activatedRouteProp("paramMap") and kind.isPath()
8484
or
85-
result = urlSegment().getAPropertyRead("parameterMap")
85+
result = activatedRouteProp("queryParamMap") and kind.isQuery()
86+
or
87+
result = urlSegment().getAPropertyRead("parameterMap") and kind.isPath()
8688
}
8789

90+
/** Gets a reference to a `ParamMap` object, usually containing values from the URL. */
91+
DataFlow::SourceNode paramMap() { result = paramMap(_) }
92+
8893
/** Gets a reference to a `Params` object, usually containing values from the URL. */
89-
DataFlow::SourceNode paramDictionaryObject() {
94+
private DataFlow::SourceNode paramDictionaryObject(ClientSideRemoteFlowKind kind) {
9095
result.hasUnderlyingType("@angular/router", "Params") and
96+
kind.isPath() and
9197
not result instanceof DataFlow::ObjectLiteralNode // ignore object literals found by contextual typing
9298
or
93-
result = activatedRouteProp(["params", "queryParams"])
99+
result = activatedRouteProp("params") and kind.isPath()
100+
or
101+
result = activatedRouteProp("queryParams") and kind.isQuery()
94102
or
95-
result = paramMap().getAPropertyRead("params")
103+
result = paramMap(kind).getAPropertyRead("params")
96104
or
97-
result = urlSegment().getAPropertyRead("parameters")
105+
result = urlSegment().getAPropertyRead("parameters") and kind.isPath()
98106
}
99107

108+
/** Gets a reference to a `Params` object, usually containing values from the URL. */
109+
DataFlow::SourceNode paramDictionaryObject() { result = paramDictionaryObject(_) }
110+
100111
/**
101112
* A value from `@angular/router` derived from the URL.
102113
*/
103-
class AngularSource extends RemoteFlowSource {
114+
class AngularSource extends ClientSideRemoteFlowSource {
115+
ClientSideRemoteFlowKind kind;
116+
104117
AngularSource() {
105-
this = paramMap().getAMethodCall(["get", "getAll"])
118+
this = paramMap(kind).getAMethodCall(["get", "getAll"])
106119
or
107-
this = paramDictionaryObject()
120+
this = paramDictionaryObject(kind)
108121
or
109-
this = activatedRouteProp("fragment")
122+
this = activatedRouteProp("fragment") and kind.isFragment()
110123
or
111-
this = urlSegment().getAPropertyRead("path")
124+
this = urlSegment().getAPropertyRead("path") and kind.isPath()
112125
or
113126
// Note that Router.url and RouterStateSnapshot.url are strings, not UrlSegment[]
114-
this = router().getAPropertyRead("url")
127+
this = router().getAPropertyRead("url") and kind.isUrl()
115128
or
116-
this = routerStateSnapshot().getAPropertyRead("url")
129+
this = routerStateSnapshot().getAPropertyRead("url") and kind.isUrl()
117130
}
118131

119132
override string getSourceType() { result = "Angular route parameter" }
133+
134+
override ClientSideRemoteFlowKind getKind() { result = kind }
120135
}
121136

122137
/** Gets a reference to a `DomSanitizer` object. */

javascript/ql/src/semmle/javascript/frameworks/AngularJS/AngularJSCore.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ private class LocationFlowSource extends RemoteFlowSource {
636636
*
637637
* See https://docs.angularjs.org/api/ngRoute/service/$routeParams for more details.
638638
*/
639-
private class RouteParamSource extends RemoteFlowSource {
639+
private class RouteParamSource extends ClientSideRemoteFlowSource {
640640
RouteParamSource() {
641641
exists(ServiceReference service |
642642
service.getName() = "$routeParams" and
@@ -645,6 +645,8 @@ private class RouteParamSource extends RemoteFlowSource {
645645
}
646646

647647
override string getSourceType() { result = "$routeParams" }
648+
649+
override ClientSideRemoteFlowKind getKind() { result.isPath() }
648650
}
649651

650652
/**

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,14 +686,24 @@ private DataFlow::SourceNode reactRouterDom() {
686686
result = DataFlow::moduleImport("react-router-dom")
687687
}
688688

689-
private class ReactRouterSource extends RemoteFlowSource {
689+
private class ReactRouterSource extends ClientSideRemoteFlowSource {
690+
ClientSideRemoteFlowKind kind;
691+
690692
ReactRouterSource() {
691-
this = reactRouterDom().getAMemberCall("useParams")
693+
this = reactRouterDom().getAMemberCall("useParams") and kind.isPath()
692694
or
693-
this = reactRouterDom().getAMemberCall("useRouteMatch").getAPropertyRead(["params", "url"])
695+
exists(string prop |
696+
this = reactRouterDom().getAMemberCall("useRouteMatch").getAPropertyRead(prop)
697+
|
698+
prop = "params" and kind.isPath()
699+
or
700+
prop = "url" and kind.isUrl()
701+
)
694702
}
695703

696704
override string getSourceType() { result = "react-router path parameters" }
705+
706+
override ClientSideRemoteFlowKind getKind() { result = kind }
697707
}
698708

699709
/**

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@ module ClientSideUrlRedirect {
2525
class Configuration extends TaintTracking::Configuration {
2626
Configuration() { this = "ClientSideUrlRedirect" }
2727

28-
override predicate isSource(DataFlow::Node source) { source instanceof Source }
29-
3028
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel lbl) {
31-
source = DOM::locationSource() and
32-
lbl instanceof DocumentUrl
29+
source.(Source).getAFlowLabel() = lbl
3330
}
3431

3532
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }

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

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ module ClientSideUrlRedirect {
1313
/**
1414
* A data flow source for unvalidated URL redirect vulnerabilities.
1515
*/
16-
abstract class Source extends DataFlow::Node { }
16+
abstract class Source extends DataFlow::Node {
17+
/** Gets a flow label to associate with this source. */
18+
DataFlow::FlowLabel getAFlowLabel() { result.isTaint() }
19+
}
1720

1821
/**
1922
* A data flow sink for unvalidated URL redirect vulnerabilities.
@@ -35,22 +38,32 @@ module ClientSideUrlRedirect {
3538

3639
/** A source of remote user input, considered as a flow source for unvalidated URL redirects. */
3740
class RemoteFlowSourceAsSource extends Source {
38-
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
41+
RemoteFlowSourceAsSource() {
42+
this instanceof RemoteFlowSource and
43+
not this.(ClientSideRemoteFlowSource).getKind().isPath()
44+
}
45+
46+
override DataFlow::FlowLabel getAFlowLabel() {
47+
if this.(ClientSideRemoteFlowSource).getKind().isUrl()
48+
then result instanceof DocumentUrl
49+
else result.isTaint()
50+
}
3951
}
4052

4153
/**
42-
* Holds if `queryAccess` is an expression that may access the query string
43-
* of a URL that flows into `nd` (that is, the part after the `?`).
54+
* DEPRECATED. Can usually be replaced with `untrustedUrlSubstring`.
55+
* Query accesses via `location.hash` or `location.search` are now independent
56+
* `RemoteFlowSource` instances, and only substrings of `location` need to be handled via steps.
4457
*/
45-
predicate queryAccess(DataFlow::Node nd, DataFlow::Node queryAccess) {
46-
exists(string propertyName |
47-
queryAccess.asExpr().(PropAccess).accesses(nd.asExpr(), propertyName)
48-
|
49-
propertyName = "search" or propertyName = "hash"
50-
)
51-
or
58+
deprecated predicate queryAccess = untrustedUrlSubstring/2;
59+
60+
/**
61+
* Holds if `substring` refers to a substring of `base` which is considered untrusted
62+
* when `base` is the current URL.
63+
*/
64+
predicate untrustedUrlSubstring(DataFlow::Node base, DataFlow::Node substring) {
5265
exists(MethodCallExpr mce, string methodName |
53-
mce = queryAccess.asExpr() and mce.calls(nd.asExpr(), methodName)
66+
mce = substring.asExpr() and mce.calls(base.asExpr(), methodName)
5467
|
5568
methodName = "split" and
5669
// exclude all splits where only the prefix is accessed, which is safe for url-redirects.
@@ -63,17 +76,12 @@ module ClientSideUrlRedirect {
6376
)
6477
or
6578
exists(MethodCallExpr mce |
66-
queryAccess.asExpr() = mce and
79+
substring.asExpr() = mce and
6780
mce = any(DataFlow::RegExpCreationNode re).getAMethodCall("exec").asExpr() and
68-
nd.asExpr() = mce.getArgument(0)
81+
base.asExpr() = mce.getArgument(0)
6982
)
7083
}
7184

72-
/**
73-
* A sanitizer that reads the first part a location split by "?", e.g. `location.href.split('?')[0]`.
74-
*/
75-
class QueryPrefixSanitizer extends Sanitizer, DomBasedXss::QueryPrefixSanitizer { }
76-
7785
/**
7886
* A sink which is used to set the window location.
7987
*/

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ module CodeInjection {
2424

2525
override predicate isSanitizer(DataFlow::Node node) {
2626
super.isSanitizer(node) or
27-
isSafeLocationProperty(node.asExpr()) or
2827
node instanceof Sanitizer
2928
}
3029

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,6 @@ module CodeInjection {
3232
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
3333
}
3434

35-
/**
36-
* An access to a property that may hold (parts of) the document URL.
37-
*/
38-
class LocationSource extends Source {
39-
LocationSource() { this = DOM::locationSource() }
40-
}
41-
4235
/**
4336
* An expression which may be interpreted as an AngularJS expression.
4437
*/

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ module CommandInjection {
2828

2929
/** A source of remote user input, considered as a flow source for command injection. */
3030
class RemoteFlowSourceAsSource extends Source {
31-
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
31+
RemoteFlowSourceAsSource() {
32+
this instanceof RemoteFlowSource and
33+
not this instanceof ClientSideRemoteFlowSource
34+
}
3235

3336
override string getSourceType() { result = "a user-provided value" }
3437
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ module CorsMisconfigurationForCredentials {
2929

3030
/** A source of remote user input, considered as a flow source for CORS misconfiguration. */
3131
class RemoteFlowSourceAsSource extends Source {
32-
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
32+
RemoteFlowSourceAsSource() {
33+
this instanceof RemoteFlowSource and
34+
not this instanceof ClientSideRemoteFlowSource
35+
}
3336
}
3437

3538
/**

0 commit comments

Comments
 (0)