Skip to content

Commit 8d37c03

Browse files
committed
using pseudo-properties to model URL parsing
1 parent 4b89eee commit 8d37c03

File tree

3 files changed

+86
-22
lines changed

3 files changed

+86
-22
lines changed

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

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -602,45 +602,89 @@ module TaintTracking {
602602
}
603603

604604
/**
605-
* Holds if `params` is a `URLSearchParams` object providing access to
606-
* the parameters encoded in `input`.
605+
* Holds if `params` is a construction of a `URLSearchParams` that parses
606+
* the parameters in `input`.
607607
*/
608-
predicate isUrlSearchParams(DataFlow::SourceNode params, DataFlow::Node input) {
608+
private predicate isUrlSearchParams(DataFlow::SourceNode params, DataFlow::Node input) {
609609
exists(DataFlow::GlobalVarRefNode urlSearchParams, NewExpr newUrlSearchParams |
610610
urlSearchParams.getName() = "URLSearchParams" and
611611
newUrlSearchParams = urlSearchParams.getAnInstantiation().asExpr() and
612612
params.asExpr() = newUrlSearchParams and
613613
input.asExpr() = newUrlSearchParams.getArgument(0)
614614
)
615-
or
616-
exists(DataFlow::NewNode newUrl |
617-
newUrl = DataFlow::globalVarRef("URL").getAnInstantiation() and
618-
params = newUrl.getAPropertyRead("searchParams") and
619-
input = newUrl.getArgument(0)
620-
)
621615
}
622616

623617
/**
624-
* A taint propagating data flow edge arising from URL parameter parsing.
618+
* A pseudo-property used to store a value on a `URLSearchParams` that
619+
* can be obtained with a `get` or `getAll` call.
625620
*/
626-
private class UrlSearchParamsTaintStep extends AdditionalTaintStep, DataFlow::ValueNode {
627-
DataFlow::Node source;
621+
private string hiddenUrlPseudoProperty() {
622+
result = "$hiddenSearchPararms"
623+
}
628624

625+
/**
626+
* A taint propagating data flow edge arising from URL parameter parsing.
627+
*/
628+
private class UrlSearchParamsTaintStep extends DataFlow::AdditionalFlowStep {
629629
UrlSearchParamsTaintStep() {
630-
// either this is itself an `URLSearchParams` object
631-
isUrlSearchParams(this, source)
632-
or
633-
// or this is a call to `get` or `getAll` on a `URLSearchParams` object
634-
exists(DataFlow::SourceNode searchParams, string m |
635-
isUrlSearchParams(searchParams, source) and
636-
this = searchParams.getAMethodCall(m) and
637-
m.matches("get%")
638-
)
630+
this = DataFlow::globalVarRef("URL") or
631+
this = DataFlow::globalVarRef("URLSearchParams")
639632
}
640633

634+
/**
635+
* Holds if `succ` is a `URLSearchParams` providing access to the
636+
* parameters encoded in `pred`.
637+
*/
641638
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
642-
pred = source and succ = this
639+
isUrlSearchParams(succ, pred)
640+
}
641+
642+
/**
643+
* Holds if `pred` should be stored in the object `succ` under the property `prop`.
644+
*
645+
* This step is used to model 2 facts:
646+
* 1) A `URL` constructed using `url = new URL(input)` transfers taint from `input` to `url.searchParams`.
647+
* 2) A `URLSearchParams` (either `url.searchParams` or `new URLSearchParams(input)`) has a tainted value,
648+
* which is stored in a pseudo-property, that can later be access using a `get` or `getAll` call.
649+
*/
650+
override predicate storeStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
651+
(prop = "searchParams" or prop = hiddenUrlPseudoProperty()) and
652+
exists(DataFlow::NewNode newUrl | succ = newUrl |
653+
newUrl = DataFlow::globalVarRef("URL").getAnInstantiation() and
654+
pred = newUrl.getArgument(0)
655+
)
656+
or
657+
prop = hiddenUrlPseudoProperty() and
658+
isUrlSearchParams(succ, pred)
643659
}
660+
661+
/**
662+
* Holds if the property `prop` should be copied from the object `pred` to the object `succ`.
663+
*
664+
* This step is used to copy a value the value of our pseudo-property that can later be accessed using a `get` or `getAll` call.
665+
*/
666+
override predicate loadStoreStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
667+
prop = hiddenUrlPseudoProperty() and
668+
exists(DataFlow::PropRead write | write = succ |
669+
write.getPropertyName() = "searchParams" and
670+
write.getBase() = pred
671+
)
672+
}
673+
674+
/**
675+
* Holds if the property `prop` of the object `pred` should be loaded into `succ`.
676+
*
677+
* This step is used to load the value stored in the hidden pseudo-property.
678+
*/
679+
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
680+
prop = hiddenUrlPseudoProperty() and
681+
// this is a call to `get` or `getAll` on a `URLSearchParams` object
682+
exists(string m, DataFlow::MethodCallNode call | call = succ |
683+
call.getMethodName() = m and
684+
call.getReceiver() = pred and
685+
m.matches("get%")
686+
)
687+
}
644688
}
645689

646690
/**

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ 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') |
336340
| typeahead.js:20:13:20:45 | target |
337341
| typeahead.js:20:22:20:38 | document.location |
338342
| typeahead.js:20:22:20:38 | document.location |
@@ -642,6 +646,10 @@ edges
642646
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
643647
| tst.js:313:10:313:10 | e | tst.js:314:20:314:20 | e |
644648
| tst.js:319:35:319:42 | location | tst.js:319:35:319:42 | location |
649+
| tst.js:330:18:330:34 | document.location | tst.js:336:18:336:35 | params.get('name') |
650+
| tst.js:330:18:330:34 | document.location | tst.js:336:18:336:35 | params.get('name') |
651+
| tst.js:330:18:330:34 | document.location | tst.js:336:18:336:35 | params.get('name') |
652+
| tst.js:330:18:330:34 | document.location | tst.js:336:18:336:35 | params.get('name') |
645653
| typeahead.js:20:13:20:45 | target | typeahead.js:21:12:21:17 | target |
646654
| typeahead.js:20:22:20:38 | document.location | typeahead.js:20:22:20:45 | documen ... .search |
647655
| typeahead.js:20:22:20:38 | document.location | typeahead.js:20:22:20:45 | documen ... .search |
@@ -741,6 +749,7 @@ edges
741749
| 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 |
742750
| 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 |
743751
| 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 |
752+
| 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 |
744753
| 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 |
745754
| 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 |
746755
| 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/tst.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,15 @@ 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+
327338
}

0 commit comments

Comments
 (0)