-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathDOM.qll
More file actions
556 lines (490 loc) · 18.5 KB
/
DOM.qll
File metadata and controls
556 lines (490 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
/**
* Provides classes for working with DOM elements.
*/
import javascript
import semmle.javascript.frameworks.Templating
private import semmle.javascript.dataflow.InferredTypes
module DOM {
/**
* A definition of a DOM element, for instance by an HTML element in an HTML file
* or a JSX element in a JavaScript file.
*/
abstract class ElementDefinition extends Locatable {
/**
* Gets the name of the DOM element; for example, a `<p>` element has
* name `p`.
*/
abstract string getName();
/**
* Gets the `i`th attribute of this DOM element, if it can be determined.
*
* For example, the 0th (and only) attribute of `<a href="https://semmle.com">Semmle</a>`
* is `href="https://semmle.com"`.
*/
AttributeDefinition getAttribute(int i) { none() }
/**
* Gets an attribute of this DOM element with name `name`.
*
* For example, the DOM element `<a href="https://semmle.com">Semmle</a>`
* has a single attribute `href="https://semmle.com"` with the name `href`.
*/
AttributeDefinition getAttributeByName(string name) {
result.getElement() = this and
result.getName() = name
}
/**
* Gets an attribute of this DOM element.
*/
AttributeDefinition getAnAttribute() { result.getElement() = this }
/**
* Gets the parent element of this element.
*/
abstract ElementDefinition getParent();
/**
* Gets the root element (i.e. an element without a parent) in which this element is contained.
*/
ElementDefinition getRoot() {
if not exists(this.getParent()) then result = this else result = this.getParent().getRoot()
}
/**
* Gets the document element to which this element belongs, if it can be determined.
*/
DocumentElementDefinition getDocument() { result = this.getRoot() }
}
/**
* An HTML element, viewed as an `ElementDefinition`.
*/
private class HtmlElementDefinition extends ElementDefinition, @xmlelement instanceof HTML::Element
{
override string getName() { result = HTML::Element.super.getName() }
override AttributeDefinition getAttribute(int i) {
result = HTML::Element.super.getAttribute(i)
}
override ElementDefinition getParent() { result = HTML::Element.super.getParent() }
}
/**
* A JSX element, viewed as an `ElementDefinition`.
*/
private class JsxElementDefinition extends ElementDefinition, @jsx_element instanceof JsxElement {
override string getName() { result = JsxElement.super.getName() }
override AttributeDefinition getAttribute(int i) { result = JsxElement.super.getAttribute(i) }
override ElementDefinition getParent() { result = super.getJsxParent() }
}
/**
* A DOM attribute as defined, for instance, by an HTML attribute in an HTML file
* or a JSX attribute in a JavaScript file.
*/
abstract class AttributeDefinition extends Locatable {
/**
* Gets the name of this attribute, if any.
*
* JSX spread attributes do not have a name.
*/
abstract string getName();
/**
* Gets the data flow node whose value is the value of this attribute,
* if any.
*
* This is undefined for HTML elements, where the attribute value is not
* computed but specified directly.
*/
DataFlow::Node getValueNode() { none() }
/**
* Gets the value of this attribute, if it can be determined.
*/
string getStringValue() { result = this.getValueNode().getStringValue() }
/**
* Gets the DOM element this attribute belongs to.
*/
ElementDefinition getElement() { this = result.getAttributeByName(_) }
/**
* Holds if the value of this attribute might be a template value
* such as `{{window.location.url}}`.
*/
predicate mayHaveTemplateValue() {
this.getStringValue().regexpMatch(Templating::getDelimiterMatchingRegexp())
}
}
/**
* An HTML attribute, viewed as an `AttributeDefinition`.
*/
private class HtmlAttributeDefinition extends AttributeDefinition, @xmlattribute instanceof HTML::Attribute
{
override string getName() { result = HTML::Attribute.super.getName() }
override string getStringValue() { result = super.getValue() }
override ElementDefinition getElement() { result = HTML::Attribute.super.getElement() }
}
/**
* A JSX attribute, viewed as an `AttributeDefinition`.
*/
private class JsxAttributeDefinition extends AttributeDefinition, @jsx_attribute instanceof JsxAttribute
{
override string getName() { result = JsxAttribute.super.getName() }
override DataFlow::Node getValueNode() {
result = DataFlow::valueNode(JsxAttribute.super.getValue())
}
override ElementDefinition getElement() { result = JsxAttribute.super.getElement() }
}
/**
* An HTML `<document>` element.
*/
class DocumentElementDefinition extends ElementDefinition {
DocumentElementDefinition() { this.getName() = "html" }
override string getName() { none() }
override AttributeDefinition getAttribute(int i) { none() }
override AttributeDefinition getAttributeByName(string name) { none() }
override ElementDefinition getParent() { none() }
}
/**
* Holds if the value of attribute `attr` is interpreted as a URL.
*/
predicate isUrlValuedAttribute(AttributeDefinition attr) {
exists(string eltName, string attrName |
eltName = attr.getElement().getName() and
attrName = attr.getName()
|
eltName = ["script", "iframe", "embed", "video", "audio", "source", "track"] and
attrName = "src"
or
eltName = ["link", "a", "base", "area"] and
attrName = "href"
or
eltName = "form" and
attrName = "action"
or
eltName = ["input", "button"] and
attrName = "formaction"
)
}
/**
* A data flow node or other program element that may refer to
* a DOM element.
*/
overlay[global]
abstract class Element extends Locatable {
ElementDefinition defn;
/** Gets the definition of this element. */
ElementDefinition getDefinition() { result = defn }
/** Gets the tag name of this DOM element. */
string getName() { result = defn.getName() }
/** Gets the `i`th attribute of this DOM element, if it can be determined. */
AttributeDefinition getAttribute(int i) { result = defn.getAttribute(i) }
/** Gets an attribute of this DOM element with the given `name`. */
AttributeDefinition getAttributeByName(string name) { result = defn.getAttributeByName(name) }
}
/**
* The default implementation of `Element`, including both
* element definitions and data flow nodes that may refer to them.
*/
private class DefaultElement extends Element {
DefaultElement() {
defn = this
or
exists(Element that |
this.(Expr).flow().getALocalSource().asExpr() = that and
defn = that.getDefinition()
)
}
}
/**
* Holds if `attr` is an invalid id attribute because of `reason`.
*/
predicate isInvalidHtmlIdAttributeValue(DOM::AttributeDefinition attr, string reason) {
attr.getName() = "id" and
exists(string v | v = attr.getStringValue() |
v = "" and
reason = "must contain at least one character"
or
v.regexpMatch(".*\\s.*") and
reason = "must not contain any space characters"
)
}
/** Gets a call that queries the DOM for a collection of DOM nodes. */
private DataFlow::SourceNode domElementCollection() {
exists(string collectionName |
collectionName =
[
"getElementsByClassName", "getElementsByName", "getElementsByTagName",
"getElementsByTagNameNS", "querySelectorAll"
]
|
(
result = domValueRef().getAMethodCall(collectionName) or
result = DataFlow::globalVarRef(collectionName).getACall()
)
)
}
/** Gets a call that creates a DOM node or queries the DOM for a DOM node. */
private DataFlow::SourceNode domElementCreationOrQuery() {
exists(string methodName |
methodName =
["createElement", "createElementNS", "createRange", "getElementById", "querySelector"]
|
result = documentRef().getAMethodCall(methodName) or
result = DataFlow::globalVarRef(methodName).getACall()
)
}
module DomValueSource {
/**
* A data flow node that should be considered a source of DOM values.
*/
abstract class Range extends DataFlow::Node { }
private predicate isDomElementType(ExternalType type) { isDomRootType(type.getASupertype*()) }
private string getADomPropertyName() {
exists(ExternalInstanceMemberDecl decl |
result = decl.getName() and
isDomElementType(decl.getDeclaringType())
)
}
private predicate isDomElementTypeName(string name) {
exists(ExternalType type |
isDomElementType(type) and
name = type.getName()
)
}
/** Gets a method name which, if invoked on a DOM element (possibly of a specific subtype), returns a DOM element. */
private string getAMethodProducingDomElements() {
exists(ExternalInstanceMemberDecl decl |
result = decl.getName() and
isDomElementType(decl.getDeclaringType()) and
isDomElementTypeName(decl.getDocumentation()
.getATagByTitle("return")
.getType()
.getAnUnderlyingType()
.(JSDocNamedTypeExpr)
.getRawName())
)
}
/**
* Gets a data flow node that might refer to some form.
* Either by a read like `document.forms[0]`, or a property read from `document` with some constant property-name.
* E.g. if `<form name="foobar">..</form>` exists, then `document.foobar` refers to that form.
*/
private DataFlow::SourceNode forms() {
result = documentRef().getAPropertyRead("forms").getAPropertyRead()
or
exists(DataFlow::PropRead read |
read = documentRef().getAPropertyRead() and
result = read
|
read.mayHavePropertyName(_) and
not read.mayHavePropertyName(getADomPropertyName())
)
}
private InferredType getArgumentTypeFromJQueryMethodGet(JQuery::MethodCall call) {
call.getMethodName() = "get" and
result = call.getArgument(0).analyze().getAType()
}
private class DefaultRange extends Range {
DefaultRange() {
this.asExpr().(VarAccess).getVariable() instanceof DomGlobalVariable
or
exists(DataFlow::PropRead read |
this = read and
read = domValueRef().getAPropertyRead()
|
not read.mayHavePropertyName(_)
or
read.mayHavePropertyName(getADomPropertyName())
or
read.mayHavePropertyName(any(string s | exists(s.toInt())))
)
or
this = domElementCreationOrQuery()
or
this = domElementCollection()
or
this = domValueRef().getAMethodCall(getAMethodProducingDomElements())
or
this = forms()
or
// reading property `foo` - where a child has `name="foo"` - resolves to that child.
// We only look for such properties on forms/document, to avoid potential false positives.
exists(DataFlow::SourceNode form | form = [forms(), documentRef()] |
this = form.getAPropertyRead(any(string s | not s = getADomPropertyName()))
)
or
exists(JQuery::MethodCall call | this = call and call.getMethodName() = "get" |
call.getNumArgument() = 1 and
unique(InferredType t | t = getArgumentTypeFromJQueryMethodGet(call)) = TTNumber()
)
or
// A `this` node from a callback given to a `$().each(callback)` call.
// purposely not using JQuery::MethodCall to avoid `jquery.each()`.
exists(DataFlow::CallNode eachCall | eachCall = JQuery::objectRef().getAMethodCall("each") |
this = DataFlow::thisNode(eachCall.getCallback(0).getFunction()) or
this = eachCall.getABoundCallbackParameter(0, 1)
)
or
// A read of an array-element from a JQuery object. E.g. `$("#foo")[0]`
exists(DataFlow::PropRead read |
read = this and read = JQuery::objectRef().getAPropertyRead()
|
unique(InferredType t | t = read.getPropertyNameExpr().analyze().getAType()) = TTNumber()
)
or
// A receiver node of an event handler on a DOM node
exists(DataFlow::SourceNode domNode, DataFlow::FunctionNode eventHandler |
// NOTE: we do not use `getABoundFunctionValue()`, since bound functions tend to have
// a different receiver anyway
eventHandler = domNode.getAPropertySource(any(string n | n.matches("on%")))
or
eventHandler =
domNode.getAMethodCall("addEventListener").getArgument(1).getAFunctionValue()
|
domNode = domValueRef() and
this = eventHandler.getReceiver()
)
or
this = DataFlow::thisNode(any(EventHandlerCode evt))
}
}
}
/** A data flow node that is a source of DOM events. */
class DomEventSource extends DataFlow::Node instanceof DomEventSource::Range { }
/** Companion module to the `DomEventSource` class. */
module DomEventSource {
/**
* A data flow node that should be considered a source of DOM events.
*/
abstract class Range extends DataFlow::Node { }
private class DefaultRange extends Range {
DefaultRange() {
// e.g. <form onSubmit={e => e.target}/>
exists(JsxAttribute attr | attr.getName().matches("on%") |
this = attr.getValue().flow().getABoundFunctionValue(0).getParameter(0)
)
or
// node.addEventListener("submit", e => e.target)
this = domValueRef().getAMethodCall("addEventListener").getABoundCallbackParameter(1, 0)
or
// node.onSubmit = (e => e.target);
exists(DataFlow::PropWrite write | write = domValueRef().getAPropertyWrite() |
write.getPropertyName().matches("on%") and
this = write.getRhs().getAFunctionValue().getParameter(0)
)
}
}
}
/** Gets a data flow node that refers directly to a value from the DOM. */
DataFlow::SourceNode domValueSource() { result instanceof DomValueSource::Range }
/** Gets a data flow node that may refer to a value from the DOM. */
private DataFlow::SourceNode domValueRef(DataFlow::TypeTracker t) {
t.start() and
result = domValueSource()
or
t.start() and
result = domValueRef().getAMethodCall(["item", "namedItem"])
or
t.startInProp("target") and
result instanceof DomEventSource
or
t.startInProp(DataFlow::PseudoProperties::arrayElement()) and
result = domElementCollection()
or
exists(DataFlow::TypeTracker t2 | result = domValueRef(t2).track(t2, t))
}
/** Gets a data flow node that may refer to a value from the DOM. */
DataFlow::SourceNode domValueRef() {
result = domValueRef(DataFlow::TypeTracker::end())
or
result.hasUnderlyingType(["Element", "HTMLCollection", "HTMLCollectionOf"])
or
result.hasUnderlyingType(any(string s | s.matches("HTML%Element")))
or
result = documentRef()
or
exists(DataFlow::ClassNode cls |
cls.getASuperClassNode().getALocalSource() =
DataFlow::globalVarRef(any(string s | s.matches("HTML%Element"))) and
result = cls.getAnInstanceReference()
)
}
module LocationSource {
/**
* A data flow node that should be considered a source of the DOM `location` object.
*
* Can be subclassed to add additional such nodes.
*/
abstract class Range extends DataFlow::Node { }
private class DefaultRange extends Range {
DefaultRange() {
exists(string propName | this = documentRef().getAPropertyRead(propName) |
propName = ["documentURI", "documentURIObject", "location", "referrer", "URL"]
)
or
this = DOM::domValueRef().getAPropertyRead("baseUri")
or
this = DataFlow::globalVarRef("location")
or
this = any(DataFlow::Node n | n.hasUnderlyingType("Location")).getALocalSource() and
not this = nonFirstLocationType(DataFlow::TypeTracker::end()) // only start from the source, and not the locations we can type-track to.
}
}
}
/**
* Get a reference to a node of type `Location` that has gone through at least 1 type-tracking step.
*/
private DataFlow::SourceNode nonFirstLocationType(DataFlow::TypeTracker t) {
// One step inlined in the beginning.
exists(DataFlow::TypeTracker t2 |
result =
any(DataFlow::Node n | n.hasUnderlyingType("Location")).getALocalSource().track(t2, t) and
t2.start()
)
or
exists(DataFlow::TypeTracker t2 | result = nonFirstLocationType(t2).track(t2, t))
}
/** Gets a data flow node that directly refers to a DOM `location` object. */
DataFlow::SourceNode locationSource() { result instanceof LocationSource::Range }
/** Gets a reference to a DOM `location` object. */
private DataFlow::SourceNode locationRef(DataFlow::TypeTracker t) {
t.start() and
result = locationSource()
or
t.startInProp("location") and
result = [DataFlow::globalObjectRef(), documentSource()]
or
exists(DataFlow::TypeTracker t2 | result = locationRef(t2).track(t2, t))
}
/** Gets a reference to a DOM `location` object. */
DataFlow::SourceNode locationRef() { result = locationRef(DataFlow::TypeTracker::end()) }
module DocumentSource {
/**
* A data flow node that should be considered a source of the `document` object.
*
* Can be subclassed to add additional such nodes.
*/
abstract class Range extends DataFlow::Node { }
private class DefaultRange extends Range {
DefaultRange() { this = DataFlow::globalVarRef("document") }
}
}
/**
* Gets a direct reference to the `document` object.
*/
DataFlow::SourceNode documentSource() { result instanceof DocumentSource::Range }
/**
* Gets a reference to the `document` object.
*/
private DataFlow::SourceNode documentRef(DataFlow::TypeTracker t) {
t.start() and
result instanceof DocumentSource::Range
or
exists(DataFlow::TypeTracker t2 | result = documentRef(t2).track(t2, t))
}
/**
* Gets a reference to the 'document' object.
*/
DataFlow::SourceNode documentRef() {
result = documentRef(DataFlow::TypeTracker::end())
or
result.hasUnderlyingType("Document")
}
/**
* Holds if a value assigned to property `name` of a DOM node can be interpreted as JavaScript via the `javascript:` protocol.
*/
string getAPropertyNameInterpretedAsJavaScriptUrl() {
result = ["action", "formaction", "href", "src", "data"]
}
}