Skip to content

Commit 0feb7f8

Browse files
authored
Merge pull request github#2761 from erik-krogh/UrlSearch
Approved by asgerf
2 parents 5c920eb + 4864e77 commit 0feb7f8

File tree

9 files changed

+232
-31
lines changed

9 files changed

+232
-31
lines changed

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

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,17 @@ abstract class Configuration extends string {
262262
predicate isAdditionalLoadStoreStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
263263
none()
264264
}
265+
266+
/**
267+
* EXPERIMENTAL. This API may change in the future.
268+
*
269+
* Holds if the property `loadProp` should be copied from the object `pred` to the property `storeProp` of object `succ`.
270+
*/
271+
predicate isAdditionalLoadStoreStep(
272+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp
273+
) {
274+
none()
275+
}
265276
}
266277

267278
/**
@@ -548,6 +559,19 @@ abstract class AdditionalFlowStep extends DataFlow::Node {
548559
*/
549560
cached
550561
predicate loadStoreStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { none() }
562+
563+
/**
564+
* EXPERIMENTAL. This API may change in the future.
565+
*
566+
* Holds if the property `loadProp` should be copied from the object `pred` to the property `storeProp` of object `succ`.
567+
*/
568+
cached
569+
predicate loadStoreStep(
570+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp
571+
) {
572+
loadProp = storeProp and
573+
loadStoreStep(pred, succ, loadProp)
574+
}
551575
}
552576

553577
/**
@@ -666,7 +690,7 @@ private predicate exploratoryFlowStep(
666690
basicLoadStep(pred, succ, _) or
667691
isAdditionalStoreStep(pred, succ, _, cfg) or
668692
isAdditionalLoadStep(pred, succ, _, cfg) or
669-
isAdditionalLoadStoreStep(pred, succ, _, cfg) or
693+
isAdditionalLoadStoreStep(pred, succ, _, _, cfg) or
670694
// the following three disjuncts taken together over-approximate flow through
671695
// higher-order calls
672696
callback(pred, succ) or
@@ -910,14 +934,22 @@ private predicate isAdditionalStoreStep(
910934
}
911935

912936
/**
913-
* Holds if the property `prop` should be copied from the object `pred` to the object `succ`.
937+
* Holds if the property `loadProp` should be copied from the object `pred` to the property `storeProp` of object `succ`.
914938
*/
915939
private predicate isAdditionalLoadStoreStep(
916-
DataFlow::Node pred, DataFlow::Node succ, string prop, DataFlow::Configuration cfg
940+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp,
941+
DataFlow::Configuration cfg
917942
) {
918-
any(AdditionalFlowStep s).loadStoreStep(pred, succ, prop)
943+
any(AdditionalFlowStep s).loadStoreStep(pred, succ, loadProp, storeProp)
944+
or
945+
cfg.isAdditionalLoadStoreStep(pred, succ, loadProp, storeProp)
919946
or
920-
cfg.isAdditionalLoadStoreStep(pred, succ, prop)
947+
loadProp = storeProp and
948+
(
949+
any(AdditionalFlowStep s).loadStoreStep(pred, succ, loadProp)
950+
or
951+
cfg.isAdditionalLoadStoreStep(pred, succ, loadProp)
952+
)
921953
}
922954

923955
/**
@@ -963,7 +995,7 @@ private predicate reachableFromStoreBase(
963995
s2.getEndLabel())
964996
)
965997
or
966-
exists(PathSummary oldSummary, PathSummary newSummary |
998+
exists(PathSummary newSummary, PathSummary oldSummary |
967999
reachableFromStoreBaseStep(prop, rhs, nd, cfg, oldSummary, newSummary) and
9681000
summary = oldSummary.appendValuePreserving(newSummary)
9691001
)
@@ -980,11 +1012,15 @@ private predicate reachableFromStoreBaseStep(
9801012
string prop, DataFlow::Node rhs, DataFlow::Node nd, DataFlow::Configuration cfg,
9811013
PathSummary oldSummary, PathSummary newSummary
9821014
) {
983-
exists(DataFlow::Node mid | reachableFromStoreBase(prop, rhs, mid, cfg, oldSummary) |
1015+
exists(DataFlow::Node mid |
1016+
reachableFromStoreBase(prop, rhs, mid, cfg, oldSummary) and
9841017
flowStep(mid, cfg, nd, newSummary)
9851018
or
986-
isAdditionalLoadStoreStep(mid, nd, prop, cfg) and
987-
newSummary = PathSummary::level()
1019+
exists(string midProp |
1020+
reachableFromStoreBase(midProp, rhs, mid, cfg, oldSummary) and
1021+
isAdditionalLoadStoreStep(mid, nd, midProp, prop, cfg) and
1022+
newSummary = PathSummary::level()
1023+
)
9881024
)
9891025
}
9901026

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

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,8 @@ module TaintTracking {
558558
}
559559

560560
/**
561-
* Holds if `params` is a `URLSearchParams` object providing access to
562-
* the parameters encoded in `input`.
561+
* Holds if `params` is a construction of a `URLSearchParams` that parses
562+
* the parameters in `input`.
563563
*/
564564
predicate isUrlSearchParams(DataFlow::SourceNode params, DataFlow::Node input) {
565565
exists(DataFlow::GlobalVarRefNode urlSearchParams, NewExpr newUrlSearchParams |
@@ -568,34 +568,92 @@ module TaintTracking {
568568
params.asExpr() = newUrlSearchParams and
569569
input.asExpr() = newUrlSearchParams.getArgument(0)
570570
)
571-
or
572-
exists(DataFlow::NewNode newUrl |
573-
newUrl = DataFlow::globalVarRef("URL").getAnInstantiation() and
574-
params = newUrl.getAPropertyRead("searchParams") and
575-
input = newUrl.getArgument(0)
576-
)
577571
}
578572

573+
/**
574+
* A pseudo-property a `URL` that stores a value that can be obtained
575+
* with a `get` or `getAll` call to the `searchParams` property.
576+
*/
577+
private string hiddenUrlPseudoProperty() { result = "$hiddenSearchPararms" }
578+
579+
/**
580+
* A pseudo-property on a `URLSearchParams` that can be obtained
581+
* with a `get` or `getAll` call.
582+
*/
583+
private string getableUrlPseudoProperty() { result = "$gettableSearchPararms" }
584+
579585
/**
580586
* A taint propagating data flow edge arising from URL parameter parsing.
581587
*/
582-
private class UrlSearchParamsTaintStep extends AdditionalTaintStep, DataFlow::ValueNode {
583-
DataFlow::Node source;
588+
private class UrlSearchParamsTaintStep extends DataFlow::AdditionalFlowStep, DataFlow::ValueNode {
589+
/**
590+
* Holds if `succ` is a `URLSearchParams` providing access to the
591+
* parameters encoded in `pred`.
592+
*/
593+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
594+
isUrlSearchParams(succ, pred) and succ = this
595+
}
584596

585-
UrlSearchParamsTaintStep() {
586-
// either this is itself an `URLSearchParams` object
587-
isUrlSearchParams(this, source)
588-
or
589-
// or this is a call to `get` or `getAll` on a `URLSearchParams` object
590-
exists(DataFlow::SourceNode searchParams, string m |
591-
isUrlSearchParams(searchParams, source) and
592-
this = searchParams.getAMethodCall(m) and
593-
m.matches("get%")
597+
/**
598+
* Holds if `pred` should be stored in the object `succ` under the property `prop`.
599+
*
600+
* This step is used to model 3 facts:
601+
* 1) A `URL` constructed using `url = new URL(input)` transfers taint from `input` to `url.searchParams`, `url.hash`, and `url.search`.
602+
* 2) Accessing the `searchParams` on a `URL` results in a `URLSearchParams` object (See the loadStoreStep method on this class and hiddenUrlPseudoProperty())
603+
* 3) A `URLSearchParams` object (either `url.searchParams` or `new URLSearchParams(input)`) has a tainted value,
604+
* which can be accessed using a `get` or `getAll` call. (See getableUrlPseudoProperty())
605+
*/
606+
override predicate storeStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
607+
succ = this and
608+
(
609+
(
610+
prop = "searchParams" or
611+
prop = "hash" or
612+
prop = "search" or
613+
prop = hiddenUrlPseudoProperty()
614+
) and
615+
exists(DataFlow::NewNode newUrl | succ = newUrl |
616+
newUrl = DataFlow::globalVarRef("URL").getAnInstantiation() and
617+
pred = newUrl.getArgument(0)
618+
)
619+
or
620+
prop = getableUrlPseudoProperty() and
621+
isUrlSearchParams(succ, pred)
594622
)
595623
}
596624

597-
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
598-
pred = source and succ = this
625+
/**
626+
* Holds if the property `loadStep` should be copied from the object `pred` to the property `storeStep` of object `succ`.
627+
*
628+
* This step is used to copy the value of our pseudo-property that can later be accessed using a `get` or `getAll` call.
629+
* For an expression `url.searchParams`, the property `hiddenUrlPseudoProperty()` from the `url` object is stored in the property `getableUrlPseudoProperty()` on `url.searchParams`.
630+
*/
631+
override predicate loadStoreStep(
632+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp
633+
) {
634+
succ = this and
635+
loadProp = hiddenUrlPseudoProperty() and
636+
storeProp = getableUrlPseudoProperty() and
637+
exists(DataFlow::PropRead read | read = succ |
638+
read.getPropertyName() = "searchParams" and
639+
read.getBase() = pred
640+
)
641+
}
642+
643+
/**
644+
* Holds if the property `prop` of the object `pred` should be loaded into `succ`.
645+
*
646+
* This step is used to load the value stored in the pseudo-property `getableUrlPseudoProperty()`.
647+
*/
648+
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
649+
succ = this and
650+
prop = getableUrlPseudoProperty() and
651+
// this is a call to `get` or `getAll` on a `URLSearchParams` object
652+
exists(string m, DataFlow::MethodCallNode call | call = succ |
653+
call.getMethodName() = m and
654+
call.getReceiver() = pred and
655+
m.matches("get%")
656+
)
599657
}
600658
}
601659

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,31 @@ module DomBasedXss {
2323
or
2424
node instanceof Sanitizer
2525
}
26+
27+
override predicate isAdditionalLoadStoreStep(
28+
DataFlow::Node pred, DataFlow::Node succ, string predProp, string succProp
29+
) {
30+
exists(DataFlow::PropRead read |
31+
pred = read.getBase() and
32+
succ = read and
33+
read.getPropertyName() = "hash" and
34+
predProp = "hash" and
35+
succProp = urlSuffixPseudoProperty()
36+
)
37+
}
38+
39+
override predicate isAdditionalLoadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
40+
exists(DataFlow::MethodCallNode call, string name |
41+
name = "substr" or name = "substring" or name = "slice"
42+
|
43+
call.getMethodName() = name and
44+
not call.getArgument(0).getIntValue() = 0 and
45+
pred = call.getReceiver() and
46+
succ = call and
47+
prop = urlSuffixPseudoProperty()
48+
)
49+
}
2650
}
51+
52+
private string urlSuffixPseudoProperty() { result = "$UrlSuffix$" }
2753
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
| tst.js:4:15:4:22 | "source" | tst.js:9:7:9:24 | readTaint(tainted) |
2+
| tst.js:4:15:4:22 | "source" | tst.js:15:7:15:20 | tainted3.other |

javascript/ql/test/library-tests/CustomLoadStoreSteps/test.ql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ class Configuration extends TaintTracking::Configuration {
1919
) and
2020
prop = "bar"
2121
}
22+
23+
// calling .copy("foo", "bar") actually moves a property from "foo" to "bar".
24+
override predicate isAdditionalLoadStoreStep(
25+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp
26+
) {
27+
exists(DataFlow::MethodCallNode call |
28+
call.getMethodName() = "copy" and call = succ and pred = call.getReceiver()
29+
|
30+
call.getArgument(0).mayHaveStringValue(loadProp) and
31+
call.getArgument(1).mayHaveStringValue(storeProp)
32+
)
33+
}
2234
}
2335

2436
from DataFlow::Node pred, DataFlow::Node succ, Configuration cfg

javascript/ql/test/library-tests/CustomLoadStoreSteps/tst.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,15 @@
66
function readTaint(x) {
77
return x.foo;
88
}
9-
sink(readTaint(tainted));
9+
sink(readTaint(tainted)); // NOT OK
10+
11+
12+
var tainted2 = {myProp: source};
13+
14+
var tainted3 = tainted2.copy("myProp", "other");
15+
sink(tainted3.other); // NOT OK.
16+
17+
var tainted4 = tainted2.copy("other", "myProp"); // does nothing, there is no "other" on tainted2.
18+
sink(tainted4.other); // OK.
19+
1020
})();

javascript/ql/test/query-tests/Security/CWE-079/Xss.expected

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ nodes
333333
| tst.js:319:35:319:42 | ___location |
334334
| tst.js:319:35:319:42 | ___location |
335335
| tst.js:319:35:319:42 | ___location |
336+
| tst.js:330:18:330:34 | document.___location |
337+
| tst.js:330:18:330:34 | document.___location |
338+
| tst.js:336:18:336:35 | params.get('name') |
339+
| tst.js:336:18:336:35 | params.get('name') |
340+
| tst.js:347:20:347:36 | document.___location |
341+
| tst.js:347:20:347:36 | document.___location |
342+
| tst.js:349:5:349:30 | getUrl( ... ring(1) |
343+
| tst.js:349:5:349:30 | getUrl( ... ring(1) |
336344
| typeahead.js:20:13:20:45 | target |
337345
| typeahead.js:20:22:20:38 | document.___location |
338346
| typeahead.js:20:22:20:38 | document.___location |
@@ -643,6 +651,14 @@ edges
643651
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
644652
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
645653
| tst.js:319:35:319:42 | ___location | tst.js:319:35:319:42 | ___location |
654+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
655+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
656+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
657+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
658+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
659+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
660+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
661+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
646662
| typeahead.js:20:13:20:45 | target | typeahead.js:21:12:21:17 | target |
647663
| typeahead.js:20:22:20:38 | document.___location | typeahead.js:20:22:20:45 | documen ... .search |
648664
| typeahead.js:20:22:20:38 | document.___location | typeahead.js:20:22:20:45 | documen ... .search |
@@ -742,6 +758,8 @@ edges
742758
| tst.js:306:20:306:20 | e | tst.js:304:9:304:16 | ___location | tst.js:306:20:306:20 | e | Cross-site scripting vulnerability due to $@. | tst.js:304:9:304:16 | ___location | user-provided value |
743759
| tst.js:314:20:314:20 | e | tst.js:311:10:311:17 | ___location | tst.js:314:20:314:20 | e | Cross-site scripting vulnerability due to $@. | tst.js:311:10:311:17 | ___location | user-provided value |
744760
| tst.js:319:35:319:42 | ___location | tst.js:319:35:319:42 | ___location | tst.js:319:35:319:42 | ___location | Cross-site scripting vulnerability due to $@. | tst.js:319:35:319:42 | ___location | user-provided value |
761+
| tst.js:336:18:336:35 | params.get('name') | tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') | Cross-site scripting vulnerability due to $@. | tst.js:330:18:330:34 | document.___location | user-provided value |
762+
| tst.js:349:5:349:30 | getUrl( ... ring(1) | tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) | Cross-site scripting vulnerability due to $@. | tst.js:347:20:347:36 | document.___location | user-provided value |
745763
| typeahead.js:25:18:25:20 | val | typeahead.js:20:22:20:38 | document.___location | typeahead.js:25:18:25:20 | val | Cross-site scripting vulnerability due to $@. | typeahead.js:20:22:20:38 | document.___location | user-provided value |
746764
| v-html.vue:2:8:2:23 | v-html=tainted | v-html.vue:6:42:6:58 | document.___location | v-html.vue:2:8:2:23 | v-html=tainted | Cross-site scripting vulnerability due to $@. | v-html.vue:6:42:6:58 | document.___location | user-provided value |
747765
| winjs.js:3:43:3:49 | tainted | winjs.js:2:17:2:33 | document.___location | winjs.js:3:43:3:49 | tainted | Cross-site scripting vulnerability due to $@. | winjs.js:2:17:2:33 | document.___location | user-provided value |

javascript/ql/test/query-tests/Security/CWE-079/XssWithAdditionalSources.expected

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ nodes
333333
| tst.js:319:35:319:42 | ___location |
334334
| tst.js:319:35:319:42 | ___location |
335335
| tst.js:319:35:319:42 | ___location |
336+
| tst.js:330:18:330:34 | document.___location |
337+
| tst.js:330:18:330:34 | document.___location |
338+
| tst.js:336:18:336:35 | params.get('name') |
339+
| tst.js:336:18:336:35 | params.get('name') |
340+
| tst.js:347:20:347:36 | document.___location |
341+
| tst.js:347:20:347:36 | document.___location |
342+
| tst.js:349:5:349:30 | getUrl( ... ring(1) |
343+
| tst.js:349:5:349:30 | getUrl( ... ring(1) |
336344
| typeahead.js:9:28:9:30 | loc |
337345
| typeahead.js:9:28:9:30 | loc |
338346
| typeahead.js:10:16:10:18 | loc |
@@ -647,6 +655,14 @@ edges
647655
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
648656
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
649657
| tst.js:319:35:319:42 | ___location | tst.js:319:35:319:42 | ___location |
658+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
659+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
660+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
661+
| tst.js:330:18:330:34 | document.___location | tst.js:336:18:336:35 | params.get('name') |
662+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
663+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
664+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
665+
| tst.js:347:20:347:36 | document.___location | tst.js:349:5:349:30 | getUrl( ... ring(1) |
650666
| typeahead.js:9:28:9:30 | loc | typeahead.js:10:16:10:18 | loc |
651667
| typeahead.js:9:28:9:30 | loc | typeahead.js:10:16:10:18 | loc |
652668
| typeahead.js:9:28:9:30 | loc | typeahead.js:10:16:10:18 | loc |

javascript/ql/test/query-tests/Security/CWE-079/tst.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,28 @@ function test2() {
324324

325325
// OK
326326
$('myId').html(target.length)
327+
}
328+
329+
function getTaintedUrl() {
330+
return new URL(document.___location);
331+
}
332+
333+
function URLPseudoProperties() {
334+
// NOT OK
335+
let params = getTaintedUrl().searchParams;
336+
$('name').html(params.get('name'));
337+
338+
// OK (.get is not defined on a URL)
339+
let myUrl = getTaintedUrl();
340+
$('name').html(myUrl.get('name'));
341+
342+
}
343+
344+
345+
function hash() {
346+
function getUrl() {
347+
return new URL(document.___location);
348+
}
349+
$(getUrl().hash.substring(1)); // NOT OK
350+
327351
}

0 commit comments

Comments
 (0)