Skip to content

Commit efeff6f

Browse files
authored
Merge pull request github#5033 from asgerf/js/generalized-remote-flow-source
Approved by erik-krogh
2 parents 2e2a5d6 + 3a68ece commit efeff6f

37 files changed

+848
-1134
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
lgtm,codescanning
2+
* The security queries now distinguish more clearly between different parts of `window.location`.
3+
When the taint source of an alert is based on `window.location`, the source will usually
4+
occur closer to where user-controlled data is obtained, such as at `location.hash`.
5+
* `js/request-forgery` no longer considers client-side path parameters to be a source due to
6+
the restricted character set usable in a path, resulting in fewer false-positive results.

javascript/ql/src/semmle/javascript/Closure.qll

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ module Closure {
129129
container = result.getContainer()
130130
}
131131

132+
pragma[noinline]
133+
private ClosureRequireCall getARequireInTopLevel(ClosureModule m) { result.getTopLevel() = m }
134+
132135
/**
133136
* A module using the Closure module system, declared using `goog.module()` or `goog.declareModuleId()`.
134137
*/
@@ -146,10 +149,8 @@ module Closure {
146149
string getClosureNamespace() { result = getModuleDeclaration().getClosureNamespace() }
147150

148151
override Module getAnImportedModule() {
149-
exists(ClosureRequireCall imprt |
150-
imprt.getTopLevel() = this and
151-
result.(ClosureModule).getClosureNamespace() = imprt.getClosureNamespace()
152-
)
152+
result.(ClosureModule).getClosureNamespace() =
153+
getARequireInTopLevel(this).getClosureNamespace()
153154
}
154155

155156
/**

javascript/ql/src/semmle/javascript/ES2015Modules.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** Provides classes for working with ECMAScript 2015 modules. */
22

33
import javascript
4+
private import semmle.javascript.internal.CachedStages
45

56
/**
67
* An ECMAScript 2015 module.
@@ -654,7 +655,9 @@ abstract class ReExportDeclaration extends ExportDeclaration {
654655
ES2015Module getReExportedES2015Module() { result = getReExportedModule() }
655656

656657
/** Gets the module from which this declaration re-exports. */
658+
cached
657659
Module getReExportedModule() {
660+
Stages::Imports::ref() and
658661
result.getFile() = getEnclosingModule().resolve(getImportedPath().(PathExpr))
659662
or
660663
result = resolveFromTypeRoot()

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/internal/CachedStages.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ module Stages {
162162
exists(any(Import i).getImportedModule())
163163
or
164164
exists(DataFlow::moduleImport(_))
165+
or
166+
exists(any(ReExportDeclaration d).getReExportedModule())
165167
}
166168
}
167169

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

Lines changed: 2 additions & 5 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 }
@@ -46,7 +43,7 @@ module ClientSideUrlRedirect {
4643
override predicate isAdditionalFlowStep(
4744
DataFlow::Node pred, DataFlow::Node succ, DataFlow::FlowLabel f, DataFlow::FlowLabel g
4845
) {
49-
queryAccess(pred, succ) and
46+
untrustedUrlSubstring(pred, succ) and
5047
f instanceof DocumentUrl and
5148
g.isTaint()
5249
or

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
*/

0 commit comments

Comments
 (0)