Files
codeql/javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll
Esben Sparre Andreasen 196382bbda Remove 2020 sinks from Xss.ql
2022-04-28 14:50:08 +00:00

665 lines
23 KiB
Plaintext

/**
* Provides classes and predicates used by the XSS queries.
*/
import javascript
private import semmle.javascript.dataflow.InferredTypes
/** Provides classes and predicates shared between the XSS queries. */
module Shared {
/** A data flow source for XSS vulnerabilities. */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for XSS vulnerabilities. */
abstract class Sink extends DataFlow::Node {
/**
* Gets the kind of vulnerability to report in the alert message.
*
* Defaults to `Cross-site scripting`, but may be overriden for sinks
* that do not allow script injection, but injection of other undesirable HTML elements.
*/
string getVulnerabilityKind() { result = "Cross-site scripting" }
}
/** A sanitizer for XSS vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/** A sanitizer guard for XSS vulnerabilities. */
abstract class SanitizerGuard extends TaintTracking::SanitizerGuardNode { }
/**
* A global regexp replacement involving the `<`, `'`, or `"` meta-character, viewed as a sanitizer for
* XSS vulnerabilities.
*/
class MetacharEscapeSanitizer extends Sanitizer, StringReplaceCall {
MetacharEscapeSanitizer() {
this.isGlobal() and
(
RegExp::alwaysMatchesMetaCharacter(this.getRegExp().getRoot(), ["<", "'", "\""])
or
// or it's like a wild-card.
RegExp::isWildcardLike(this.getRegExp().getRoot())
)
}
}
/**
* A call to `encodeURI` or `encodeURIComponent`, viewed as a sanitizer for
* XSS vulnerabilities.
*/
class UriEncodingSanitizer extends Sanitizer, DataFlow::CallNode {
UriEncodingSanitizer() {
exists(string name | this = DataFlow::globalVarRef(name).getACall() |
name = "encodeURI" or name = "encodeURIComponent"
)
}
}
/**
* A call to `serialize-javascript`, which prevents XSS vulnerabilities unless
* the `unsafe` option is set to `true`.
*/
class SerializeJavascriptSanitizer extends Sanitizer, DataFlow::CallNode {
SerializeJavascriptSanitizer() {
this = DataFlow::moduleImport("serialize-javascript").getACall() and
not this.getOptionArgument(1, "unsafe").mayHaveBooleanValue(true)
}
}
private import semmle.javascript.security.dataflow.IncompleteHtmlAttributeSanitizationCustomizations::IncompleteHtmlAttributeSanitization as IncompleteHTML
/**
* A guard that checks if a string can contain quotes, which is a guard for strings that are inside a HTML attribute.
*/
class QuoteGuard extends SanitizerGuard, StringOps::Includes {
QuoteGuard() {
this.getSubstring().mayHaveStringValue("\"") and
this.getBaseString()
.getALocalSource()
.flowsTo(any(IncompleteHTML::HtmlAttributeConcatenation attributeConcat))
}
override predicate sanitizes(boolean outcome, Expr e) {
e = this.getBaseString().getEnclosingExpr() and outcome = this.getPolarity().booleanNot()
}
}
/**
* A sanitizer guard that checks for the existence of HTML chars in a string.
* E.g. `/["'&<>]/.exec(str)`.
*/
class ContainsHtmlGuard extends SanitizerGuard, StringOps::RegExpTest {
ContainsHtmlGuard() {
exists(RegExpCharacterClass regExp |
regExp = this.getRegExp() and
forall(string s | s = ["\"", "&", "<", ">"] | regExp.getAMatchedString() = s)
)
}
override predicate sanitizes(boolean outcome, Expr e) {
outcome = this.getPolarity().booleanNot() and e = this.getStringOperand().asExpr()
}
}
/** DEPRECATED: Alias for ContainsHtmlGuard */
deprecated class ContainsHTMLGuard = ContainsHtmlGuard;
/**
* Holds if `str` is used in a switch-case that has cases matching HTML escaping.
*/
private predicate isUsedInHtmlEscapingSwitch(Expr str) {
exists(SwitchStmt switch |
// "\"".charCodeAt(0) == 34, "&".charCodeAt(0) == 38, "<".charCodeAt(0) == 60
forall(int c | c = [34, 38, 60] | c = switch.getACase().getExpr().getIntValue()) and
exists(DataFlow::MethodCallNode mcn | mcn.getMethodName() = "charCodeAt" |
mcn.flowsToExpr(switch.getExpr()) and
str = mcn.getReceiver().asExpr()
)
or
forall(string c | c = ["\"", "&", "<"] | c = switch.getACase().getExpr().getStringValue()) and
(
exists(DataFlow::MethodCallNode mcn | mcn.getMethodName() = "charAt" |
mcn.flowsToExpr(switch.getExpr()) and
str = mcn.getReceiver().asExpr()
)
or
exists(DataFlow::PropRead read | exists(read.getPropertyNameExpr()) |
read.flowsToExpr(switch.getExpr()) and
str = read.getBase().asExpr()
)
)
)
}
/**
* Gets an Ssa variable that is used in a sanitizing switch statement.
* The `pragma[noinline]` is to avoid materializing a cartesian product.
*/
pragma[noinline]
private SsaVariable getAPathEscapedInSwitch() { isUsedInHtmlEscapingSwitch(result.getAUse()) }
/**
* An expression that is sanitized by a switch-case.
*/
class IsEscapedInSwitchSanitizer extends Sanitizer {
IsEscapedInSwitchSanitizer() { this.asExpr() = getAPathEscapedInSwitch().getAUse() }
}
}
/** Provides classes and predicates for the DOM-based XSS query. */
module DomBasedXss {
/** A data flow source for DOM-based XSS vulnerabilities. */
abstract class Source extends Shared::Source { }
/** A data flow sink for DOM-based XSS vulnerabilities. */
abstract class Sink extends Shared::Sink { }
/** A sanitizer for DOM-based XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for DOM-based XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/**
* An expression whose value is interpreted as HTML
* and may be inserted into the DOM through a library.
*/
class LibrarySink extends Sink {
LibrarySink() {
// call to a jQuery method that interprets its argument as HTML
exists(JQuery::MethodCall call |
call.interpretsArgumentAsHtml(this) and
not call.interpretsArgumentAsSelector(this) // Handled by `JQuerySelectorSink`
)
or
// call to an Angular method that interprets its argument as HTML
any(AngularJS::AngularJSCall call).interpretsArgumentAsHtml(this.asExpr())
or
// call to a WinJS function that interprets its argument as HTML
exists(DataFlow::MethodCallNode mcn, string m |
m = "setInnerHTMLUnsafe" or m = "setOuterHTMLUnsafe"
|
mcn.getMethodName() = m and
this = mcn.getArgument(1)
)
or
this = any(Typeahead::TypeaheadSuggestionFunction f).getAReturn()
or
this = any(Handlebars::SafeString s).getAnArgument()
}
}
/**
* Holds if `prefix` is a prefix of `htmlString`, which may be intepreted as
* HTML by a jQuery method.
*/
predicate isPrefixOfJQueryHtmlString(DataFlow::Node htmlString, DataFlow::Node prefix) {
prefix = getAPrefixOfJQuerySelectorString(htmlString)
}
/**
* Holds if `prefix` is a prefix of `htmlString`, which may be intepreted as
* HTML by a jQuery method.
*/
private DataFlow::Node getAPrefixOfJQuerySelectorString(DataFlow::Node htmlString) {
any(JQuery::MethodCall call).interpretsArgumentAsSelector(htmlString) and
result = htmlString
or
exists(DataFlow::Node pred | pred = getAPrefixOfJQuerySelectorString(htmlString) |
result = StringConcatenation::getFirstOperand(pred)
or
result = pred.getAPredecessor()
)
}
/**
* An argument to the jQuery `$` function or similar, which is interpreted as either a selector
* or as an HTML string depending on its first character.
*/
class JQueryHtmlOrSelectorArgument extends DataFlow::Node {
JQueryHtmlOrSelectorArgument() {
exists(JQuery::MethodCall call |
call.interpretsArgumentAsHtml(this) and
call.interpretsArgumentAsSelector(this) and
pragma[only_bind_out](this.analyze()).getAType() = TTString()
)
}
/** Gets a string that flows to the prefix of this argument. */
string getAPrefix() { result = getAPrefixOfJQuerySelectorString(this).getStringValue() }
}
/**
* An argument to the jQuery `$` function or similar, which may be interpreted as HTML.
*
* This is the same as `JQueryHtmlOrSelectorArgument`, excluding cases where the value
* is prefixed by something other than `<`.
*/
class JQueryHtmlOrSelectorSink extends Sink, JQueryHtmlOrSelectorArgument {
JQueryHtmlOrSelectorSink() {
// If a prefix of the string is known, it must start with '<' or be an empty string
forall(string strval | strval = this.getAPrefix() | strval.regexpMatch("(?s)\\s*<.*|"))
}
}
import ClientSideUrlRedirectCustomizations::ClientSideUrlRedirect as ClientSideUrlRedirect
/**
* A write to a URL which may execute JavaScript code.
*/
class WriteURLSink extends Sink instanceof ClientSideUrlRedirect::Sink {
WriteURLSink() { super.isXssSink() }
}
/**
* An expression whose value is interpreted as HTML or CSS
* and may be inserted into the DOM.
*/
class DomSink extends Sink {
DomSink() {
// Call to a DOM function that inserts its argument into the DOM
any(DomMethodCallExpr call).interpretsArgumentsAsHtml(this.asExpr())
or
// Assignment to a dangerous DOM property
exists(DomPropWriteNode pw |
pw.interpretsValueAsHtml() and
this = DataFlow::valueNode(pw.getRhs())
)
or
// `html` or `source.html` properties of React Native `WebView`
exists(ReactNative::WebViewElement webView, DataFlow::SourceNode source |
source = webView or
source = webView.getAPropertyWrite("source").getRhs().getALocalSource()
|
this = source.getAPropertyWrite("html").getRhs()
)
}
}
/**
* An expression whose value is interpreted as HTML.
*/
class HtmlParserSink extends Sink {
HtmlParserSink() {
exists(DataFlow::GlobalVarRefNode domParser |
domParser.getName() = "DOMParser" and
this = domParser.getAnInstantiation().getAMethodCall("parseFromString").getArgument(0)
)
or
exists(DataFlow::MethodCallNode ccf |
isDomValue(ccf.getReceiver().asExpr()) and
ccf.getMethodName() = "createContextualFragment" and
this = ccf.getArgument(0)
)
}
}
/**
* A React `dangerouslySetInnerHTML` attribute, viewed as an XSS sink.
*
* Any write to the `__html` property of an object assigned to this attribute
* is considered an XSS sink.
*/
class DangerouslySetInnerHtmlSink extends Sink, DataFlow::ValueNode {
DangerouslySetInnerHtmlSink() {
exists(DataFlow::Node danger, DataFlow::SourceNode valueSrc |
exists(JsxAttribute attr |
attr.getName() = "dangerouslySetInnerHTML" and
attr.getValue() = danger.asExpr()
)
or
exists(ReactElementDefinition def, DataFlow::ObjectLiteralNode props |
props.flowsTo(def.getProps()) and
props.hasPropertyWrite("dangerouslySetInnerHTML", danger)
)
|
valueSrc.flowsTo(danger) and
valueSrc.hasPropertyWrite("__html", this)
)
}
}
/**
* A React tooltip where the `data-html` attribute is set to `true`.
*/
class TooltipSink extends Sink {
TooltipSink() {
exists(JsxElement el |
el.getAttributeByName("data-html").getStringValue() = "true" or
el.getAttributeByName("data-html").getValue().mayHaveBooleanValue(true)
|
this = el.getAttributeByName("data-tip").getValue().flow()
)
}
}
/**
* The HTML body of an email, viewed as an XSS sink.
*/
class EmailHtmlBodySink extends Sink {
EmailHtmlBodySink() { this = any(EmailSender sender).getHtmlBody() }
override string getVulnerabilityKind() { result = "HTML injection" }
}
/**
* A write to the `template` option of a Vue instance, viewed as an XSS sink.
*/
class VueTemplateSink extends Sink {
VueTemplateSink() {
// Note: don't use Vue::Component#getTemplate as it includes an unwanted getALocalSource() step
this = any(Vue::Component c).getOption("template")
}
}
/**
* The tag name argument to the `createElement` parameter of the
* `render` method of a Vue instance, viewed as an XSS sink.
*/
class VueCreateElementSink extends Sink {
VueCreateElementSink() {
exists(Vue::Component c, DataFlow::FunctionNode f |
f.flowsTo(c.getRender()) and
this = f.getParameter(0).getACall().getArgument(0)
)
}
}
/**
* A Vue `v-html` attribute, viewed as an XSS sink.
*/
class VHtmlSink extends Vue::VHtmlAttribute, Sink { }
/**
* A raw interpolation tag in a template file, viewed as an XSS sink.
*/
class TemplateSink extends Sink {
TemplateSink() {
exists(Templating::TemplatePlaceholderTag tag |
tag.isRawInterpolation() and
this = tag.asDataFlowNode()
)
}
}
/**
* A value being piped into the `safe` pipe in a template file,
* disabling subsequent HTML escaping.
*/
class SafePipe extends Sink {
SafePipe() { this = Templating::getAPipeCall("safe").getArgument(0) }
}
/**
* A property read from a safe property is considered a sanitizer.
*/
class SafePropertyReadSanitizer extends Sanitizer, DataFlow::Node {
SafePropertyReadSanitizer() {
exists(PropAccess pacc | pacc = this.asExpr() | pacc.getPropertyName() = "length")
}
}
/**
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
* XSS vulnerabilities.
*
* The XSS queries do not attempt to reason about correctness or completeness of sanitizers,
* so any such replacement stops taint propagation.
*/
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class SerializeJavascriptSanitizer extends Sanitizer, Shared::SerializeJavascriptSanitizer {
}
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
/**
* Holds if there exists two dataflow edges to `succ`, where one edges is sanitized, and the other edge starts with `pred`.
*/
predicate isOptionallySanitizedEdge(DataFlow::Node pred, DataFlow::Node succ) {
exists(HtmlSanitizerCall sanitizer |
// sanitized = sanitize ? sanitizer(source) : source;
exists(ConditionalExpr branch, Variable var, VarAccess access |
branch = succ.asExpr() and access = var.getAnAccess()
|
branch.getABranch() = access and
pred.getEnclosingExpr() = access and
sanitizer = branch.getABranch().flow() and
sanitizer.getAnArgument().getEnclosingExpr() = var.getAnAccess()
)
or
// sanitized = source; if (sanitize) {sanitized = sanitizer(source)};
exists(SsaPhiNode phi, SsaExplicitDefinition a, SsaDefinition b |
a = phi.getAnInput().getDefinition() and
b = phi.getAnInput().getDefinition() and
count(phi.getAnInput()) = 2 and
not a = b and
sanitizer = DataFlow::valueNode(a.getDef().getSource()) and
sanitizer.getAnArgument().asExpr().(VarAccess).getVariable() = b.getSourceVariable()
|
pred = DataFlow::ssaDefinitionNode(b) and
succ = DataFlow::ssaDefinitionNode(phi)
)
)
}
private class ContainsHtmlGuard extends SanitizerGuard, Shared::ContainsHtmlGuard { }
}
/** Provides classes and predicates for the reflected XSS query. */
module ReflectedXss {
/** A data flow source for reflected XSS vulnerabilities. */
abstract class Source extends Shared::Source { }
/** A data flow sink for reflected XSS vulnerabilities. */
abstract class Sink extends Shared::Sink { }
/** A sanitizer for reflected XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for reflected XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/**
* An expression that is sent as part of an HTTP response, considered as an XSS sink.
*
* We exclude cases where the route handler sets either an unknown content type or
* a content type that does not (case-insensitively) contain the string "html". This
* is to prevent us from flagging plain-text or JSON responses as vulnerable.
*/
class HttpResponseSink extends Sink, DataFlow::ValueNode {
override HTTP::ResponseSendArgument astNode;
HttpResponseSink() { not exists(getANonHtmlHeaderDefinition(astNode)) }
}
/**
* Gets a HeaderDefinition that defines a non-html content-type for `send`.
*/
HTTP::HeaderDefinition getANonHtmlHeaderDefinition(HTTP::ResponseSendArgument send) {
exists(HTTP::RouteHandler h |
send.getRouteHandler() = h and
result = nonHtmlContentTypeHeader(h)
|
// The HeaderDefinition affects a response sent at `send`.
headerAffects(result, send)
)
}
/**
* Holds if `h` may send a response with a content type other than HTML.
*/
HTTP::HeaderDefinition nonHtmlContentTypeHeader(HTTP::RouteHandler h) {
result = h.getAResponseHeader("content-type") and
not exists(string tp | result.defines("content-type", tp) | tp.regexpMatch("(?i).*html.*"))
}
/**
* Holds if a header set in `header` is likely to affect a response sent at `sender`.
*/
predicate headerAffects(HTTP::HeaderDefinition header, HTTP::ResponseSendArgument sender) {
sender.getRouteHandler() = header.getRouteHandler() and
(
// `sender` is affected by a dominating `header`.
header.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
or
// There is no dominating header, and `header` is non-local.
not isLocalHeaderDefinition(header) and
not exists(HTTP::HeaderDefinition dominatingHeader |
dominatingHeader.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
)
)
}
/**
* Holds if the HeaderDefinition `header` seems to be local.
* A HeaderDefinition is local if it dominates exactly one `ResponseSendArgument`.
*
* Recognizes variants of:
* ```
* response.writeHead(500, ...);
* response.end('Some error');
* return;
* ```
*/
predicate isLocalHeaderDefinition(HTTP::HeaderDefinition header) {
exists(ReachableBasicBlock headerBlock | headerBlock = header.getBasicBlock() |
1 =
strictcount(HTTP::ResponseSendArgument sender |
sender.getRouteHandler() = header.getRouteHandler() and
header.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
) and
// doesn't dominate something that looks like a callback.
not exists(Expr e | e instanceof Function | headerBlock.dominates(e.getBasicBlock()))
)
}
/**
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
* XSS vulnerabilities.
*
* The XSS queries do not attempt to reason about correctness or completeness of sanitizers,
* so any such replacement stops taint propagation.
*/
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class SerializeJavascriptSanitizer extends Sanitizer, Shared::SerializeJavascriptSanitizer {
}
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
private class ContainsHtmlGuard extends SanitizerGuard, Shared::ContainsHtmlGuard { }
}
/** Provides classes and predicates for the stored XSS query. */
module StoredXss {
/** A data flow source for stored XSS vulnerabilities. */
abstract class Source extends Shared::Source { }
/** A data flow sink for stored XSS vulnerabilities. */
abstract class Sink extends Shared::Sink { }
/** A sanitizer for stored XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for stored XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/** An arbitrary XSS sink, considered as a flow sink for stored XSS. */
private class AnySink extends Sink {
AnySink() { this instanceof Shared::Sink }
}
/**
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
* XSS vulnerabilities.
*
* The XSS queries do not attempt to reason about correctness or completeness of sanitizers,
* so any such replacement stops taint propagation.
*/
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class SerializeJavascriptSanitizer extends Sanitizer, Shared::SerializeJavascriptSanitizer {
}
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
private class ContainsHtmlGuard extends SanitizerGuard, Shared::ContainsHtmlGuard { }
}
/** Provides classes and predicates for the XSS through DOM query. */
module XssThroughDom {
/** A data flow source for XSS through DOM vulnerabilities. */
abstract class Source extends Shared::Source { }
}
/** Provides classes for customizing the `ExceptionXss` query. */
module ExceptionXss {
/** A data flow source for XSS caused by interpreting exception or error text as HTML. */
abstract class Source extends DataFlow::Node {
/**
* Gets a flow label to associate with this source.
*
* For sources that should pass through a `throw/catch` before reaching the sink, use the
* `NotYetThrown` labe. Otherwise use `taint` (the default).
*/
DataFlow::FlowLabel getAFlowLabel() { result.isTaint() }
/**
* Gets a human-readable description of what type of error this refers to.
*
* The result should be capitalized and usable in the context of a noun.
*/
string getDescription() { result = "Error text" }
}
/**
* A FlowLabel representing tainted data that has not been thrown in an exception.
* In the js/xss-through-exception query data-flow can only reach a sink after
* the data has been thrown as an exception, and data that has not been thrown
* as an exception therefore has this flow label, and only this flow label, associated with it.
*/
abstract class NotYetThrown extends DataFlow::FlowLabel {
NotYetThrown() { this = "NotYetThrown" }
}
private class XssSourceAsSource extends Source {
XssSourceAsSource() { this instanceof Shared::Source }
override DataFlow::FlowLabel getAFlowLabel() { result instanceof NotYetThrown }
override string getDescription() { result = "Exception text" }
}
/**
* An error produced by validating using `ajv`.
*
* Such an error can contain property names from the input if the
* underlying schema uses `additionalProperties` or `propertyPatterns`.
*
* For example, an input of form `{"<img src=x onerror=alert(1)>": 45}` might produce the error
* `data/<img src=x onerror=alert(1)> should be string`.
*/
private class JsonSchemaValidationError extends Source {
JsonSchemaValidationError() {
this = any(JsonSchema::Ajv::Instance i).getAValidationError().getAnImmediateUse()
or
this = any(JsonSchema::Joi::JoiValidationErrorRead r).getAValidationResultAccess(_)
}
override string getDescription() { result = "JSON schema validation error" }
}
}