Files
codeql/javascript/ql/lib/semmle/javascript/DOM.qll
2025-04-02 10:14:03 +02:00

556 lines
18 KiB
Plaintext

/**
* 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.
*/
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"]
}
}