Skip to content

Commit cfcd18b

Browse files
authored
Merge pull request #2429 from erik-krogh/typeAheadSink
Approved by esbena
2 parents e441e43 + ea9d618 commit cfcd18b

File tree

11 files changed

+920
-0
lines changed

11 files changed

+920
-0
lines changed

change-notes/1.24/analysis-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* Support for the following frameworks and libraries has been improved:
66
- [react](https://www.npmjs.com/package/react)
7+
- [typeahead.js](https://www.npmjs.com/package/typeahead.js)
78
- [Handlebars](https://www.npmjs.com/package/handlebars)
89

910
- Imports with the `.js` extension can now be resolved to a TypeScript file,
@@ -26,3 +27,4 @@
2627
## Changes to libraries
2728

2829
* The predicates `RegExpTerm.getSuccessor` and `RegExpTerm.getPredecessor` have been changed to reflect textual, not operational, matching order. This only makes a difference in lookbehind assertions, which are operationally matched backwards. Previously, `getSuccessor` would mimick this, so in an assertion `(?<=ab)` the term `b` would be considered the predecessor, not the successor, of `a`. Textually, however, `a` is still matched before `b`, and this is the order we now follow.
30+

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import semmle.javascript.frameworks.SQL
9292
import semmle.javascript.frameworks.SocketIO
9393
import semmle.javascript.frameworks.StringFormatters
9494
import semmle.javascript.frameworks.TorrentLibraries
95+
import semmle.javascript.frameworks.Typeahead
9596
import semmle.javascript.frameworks.UriLibraries
9697
import semmle.javascript.frameworks.Vue
9798
import semmle.javascript.frameworks.XmlParsers
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Provides classes for working with typeahead.js code (https://www.npmjs.com/package/typeahead.js).
3+
*/
4+
5+
import javascript
6+
7+
module Typeahead {
8+
/**
9+
* A reference to the Bloodhound class, which is a utility-class for generating auto-complete suggestions.
10+
*/
11+
private class Bloodhound extends DataFlow::SourceNode {
12+
Bloodhound() {
13+
this = DataFlow::moduleImport("typeahead.js/dist/bloodhound.js")
14+
or
15+
this = DataFlow::moduleImport("bloodhound-js")
16+
or
17+
this.accessesGlobal("Bloodhound")
18+
}
19+
}
20+
21+
/**
22+
* An instance of the Bloodhound class.
23+
*/
24+
private class BloodhoundInstance extends DataFlow::NewNode {
25+
BloodhoundInstance() { this = any(Bloodhound b).getAnInstantiation() }
26+
}
27+
28+
/**
29+
* An instance of of the Bloodhound class that is used to fetch data from a remote server.
30+
*/
31+
private class RemoteBloodhoundClientRequest extends ClientRequest::Range, BloodhoundInstance {
32+
DataFlow::ValueNode option;
33+
34+
RemoteBloodhoundClientRequest() {
35+
exists(string optionName | optionName = "remote" or optionName = "prefetch" |
36+
option = this.getOptionArgument(0, optionName)
37+
)
38+
}
39+
40+
/**
41+
* Gets the URL for this Bloodhound instance.
42+
* The Bloodhound API specifies that the "remote" and "prefetch" options are either strings,
43+
* or an object containing an "url" property.
44+
*/
45+
override DataFlow::Node getUrl() {
46+
result = option.getALocalSource().getAPropertyWrite("url").getRhs()
47+
or
48+
result = option
49+
}
50+
51+
override DataFlow::Node getHost() { none() }
52+
53+
override DataFlow::Node getADataNode() { none() }
54+
55+
/** Gets a Bloodhound instance that fetches remote server data. */
56+
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
57+
t.start() and result = this
58+
or
59+
exists(DataFlow::TypeTracker t2 | result = ref(t2).track(t2, t))
60+
}
61+
62+
/** Gets a Bloodhound instance that fetches remote server data. */
63+
private DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
64+
65+
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
66+
responseType = "json" and
67+
promise = false and
68+
exists(TypeaheadSource source |
69+
ref() = source.getALocalSource() or ref().getAMethodCall("ttAdapter") = source
70+
|
71+
result = source.getASuggestion()
72+
)
73+
}
74+
}
75+
76+
/**
77+
* An invocation of the `typeahead.js` library.
78+
*/
79+
private class TypeaheadCall extends DataFlow::CallNode {
80+
TypeaheadCall() {
81+
// Matches `$(...).typeahead(..)`
82+
this = JQuery::objectRef().getAMethodCall("typeahead")
83+
}
84+
}
85+
86+
/**
87+
* A function that generates suggestions to typeahead.js.
88+
*/
89+
class TypeaheadSuggestionFunction extends DataFlow::FunctionNode {
90+
TypeaheadCall typeaheadCall;
91+
92+
TypeaheadSuggestionFunction() {
93+
// Matches `$(...).typeahead({..}, { templates: { suggestion: <this> } })`.
94+
this = typeaheadCall
95+
.getOptionArgument(1, "templates")
96+
.getALocalSource()
97+
.getAPropertyWrite("suggestion")
98+
.getRhs()
99+
.getAFunctionValue()
100+
}
101+
102+
/**
103+
* Gets the call to typeahead.js where this suggestion function is used.
104+
*/
105+
TypeaheadCall getTypeaheadCall() { result = typeaheadCall }
106+
}
107+
108+
/**
109+
* A `source` option for a typeahead.js plugin instance.
110+
*/
111+
private class TypeaheadSource extends DataFlow::ValueNode {
112+
TypeaheadCall typeaheadCall;
113+
114+
TypeaheadSource() { this = typeaheadCall.getOptionArgument(1, "source") }
115+
116+
/** Gets a node for a suggestion that this source motivates. */
117+
DataFlow::Node getASuggestion() {
118+
exists(TypeaheadSuggestionFunction suggestionCallback |
119+
suggestionCallback.getTypeaheadCall() = typeaheadCall and
120+
result = suggestionCallback.getParameter(0)
121+
)
122+
}
123+
}
124+
125+
/**
126+
* A taint step that models that a function in the `source` of typeahead.js is used to determine the input to the suggestion function.
127+
*/
128+
private class TypeaheadSourceTaintStep extends TypeaheadSource, TaintTracking::AdditionalTaintStep {
129+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
130+
// Matches `$(...).typeahead({..}, {source: function(q, cb) {..;cb(<pred>);..}, templates: { suggestion: function(<succ>) {} } })`.
131+
pred = this.getAFunctionValue().getParameter([1 .. 2]).getACall().getAnArgument() and
132+
succ = this.getASuggestion()
133+
}
134+
}
135+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ module DomBasedXss {
9696
this = mcn.getArgument(1)
9797
)
9898
or
99+
this = any(Typeahead::TypeaheadSuggestionFunction f).getAReturn()
100+
or
99101
this = any(Handlebars::SafeString s).getAnArgument()
100102
}
101103
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
url
2+
| tst.js:3:14:5:6 | {\\n\\t ... \\n\\t } |
3+
| tst.js:4:15:4:52 | '/api/d ... %QUERY' |
4+
| tst.js:8:16:8:29 | searchIndexUrl |
5+
response
6+
| json | false | tst.js:15:35:15:46 | taintedParam |
7+
suggestionFunction
8+
| tst.js:15:25:17:4 | functio ... \\n\\t\\n\\t\\t\\t} |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import javascript
2+
3+
query DataFlow::Node url() {
4+
result = any(ClientRequest r).getUrl()
5+
}
6+
7+
query DataFlow::Node response(string responseType, boolean promise) {
8+
result = any(ClientRequest r).getAResponseDataNode(responseType, promise)
9+
}
10+
11+
query DataFlow::Node suggestionFunction() {
12+
result = any(Typeahead::TypeaheadSuggestionFunction t)
13+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(function () {
2+
var foo = new Bloodhound({
3+
remote: {
4+
url: '/api/destinations/search?text=%QUERY'
5+
}
6+
});
7+
var bar = new Bloodhound({
8+
prefetch: searchIndexUrl
9+
});
10+
11+
$('.typeahead').typeahead({}, {
12+
name: 'prefetchedCities',
13+
source: bar.ttAdapter(),
14+
templates: {
15+
suggestion: function (taintedParam) {
16+
17+
},
18+
}
19+
});
20+
})();

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@ nodes
331331
| tst.js:313:35:313:42 | location |
332332
| tst.js:313:35:313:42 | location |
333333
| tst.js:313:35:313:42 | location |
334+
| typeahead.js:20:13:20:45 | target |
335+
| typeahead.js:20:22:20:38 | document.location |
336+
| typeahead.js:20:22:20:38 | document.location |
337+
| typeahead.js:20:22:20:45 | documen ... .search |
338+
| typeahead.js:21:12:21:17 | target |
339+
| typeahead.js:24:30:24:32 | val |
340+
| typeahead.js:25:18:25:20 | val |
341+
| typeahead.js:25:18:25:20 | val |
334342
| v-html.vue:2:8:2:23 | v-html=tainted |
335343
| v-html.vue:2:8:2:23 | v-html=tainted |
336344
| v-html.vue:6:42:6:58 | document.location |
@@ -630,6 +638,13 @@ edges
630638
| tst.js:307:10:307:10 | e | tst.js:308:20:308:20 | e |
631639
| tst.js:307:10:307:10 | e | tst.js:308:20:308:20 | e |
632640
| tst.js:313:35:313:42 | location | tst.js:313:35:313:42 | location |
641+
| typeahead.js:20:13:20:45 | target | typeahead.js:21:12:21:17 | target |
642+
| typeahead.js:20:22:20:38 | document.location | typeahead.js:20:22:20:45 | documen ... .search |
643+
| typeahead.js:20:22:20:38 | document.location | typeahead.js:20:22:20:45 | documen ... .search |
644+
| typeahead.js:20:22:20:45 | documen ... .search | typeahead.js:20:13:20:45 | target |
645+
| typeahead.js:21:12:21:17 | target | typeahead.js:24:30:24:32 | val |
646+
| typeahead.js:24:30:24:32 | val | typeahead.js:25:18:25:20 | val |
647+
| typeahead.js:24:30:24:32 | val | typeahead.js:25:18:25:20 | val |
633648
| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted |
634649
| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted |
635650
| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted |
@@ -721,6 +736,7 @@ edges
721736
| tst.js:300:20:300:20 | e | tst.js:298:9:298:16 | location | tst.js:300:20:300:20 | e | Cross-site scripting vulnerability due to $@. | tst.js:298:9:298:16 | location | user-provided value |
722737
| tst.js:308:20:308:20 | e | tst.js:305:10:305:17 | location | tst.js:308:20:308:20 | e | Cross-site scripting vulnerability due to $@. | tst.js:305:10:305:17 | location | user-provided value |
723738
| tst.js:313:35:313:42 | location | tst.js:313:35:313:42 | location | tst.js:313:35:313:42 | location | Cross-site scripting vulnerability due to $@. | tst.js:313:35:313:42 | location | user-provided value |
739+
| 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 |
724740
| 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 |
725741
| 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 |
726742
| winjs.js:4:43:4:49 | tainted | winjs.js:2:17:2:33 | document.location | winjs.js:4:43:4:49 | tainted | Cross-site scripting vulnerability due to $@. | winjs.js:2:17:2:33 | document.location | user-provided value |

0 commit comments

Comments
 (0)