Files
codeql/javascript/ql/lib/semmle/javascript/security/dataflow/ReflectedXssCustomizations.qll
Anders Schack-Mulligen 8d97fe9ed3 JavaScript: Autoformat
2023-03-10 09:41:20 +01:00

164 lines
5.9 KiB
Plaintext

/**
* Provides default sources for reasoning about reflected
* cross-site scripting vulnerabilities.
*/
import javascript
module ReflectedXss {
private import Xss::Shared as Shared
/** 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 { }
/**
* 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 instanceof Http::ResponseSendArgument {
HttpResponseSink() { not exists(getAXssSafeHeaderDefinition(this)) }
}
/**
* DEPRECATED: Gets a HeaderDefinition that defines a non-html content-type for `send`.
*/
deprecated 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)
)
}
/**
* DEPRECATED: Holds if `h` may send a response with a content type other than HTML.
*/
deprecated 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.*"))
}
/**
* Gets a HeaderDefinition that defines a XSS safe content-type for `send`.
*/
Http::HeaderDefinition getAXssSafeHeaderDefinition(Http::ResponseSendArgument send) {
exists(Http::RouteHandler h |
send.getRouteHandler() = h and
result = xssSafeContentTypeHeader(h)
|
// The HeaderDefinition affects a response sent at `send`.
headerAffects(result, send)
)
}
/**
* Gets a content-type that may lead to javascript code being executed in the browser.
* ref: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#content-types
*/
string xssUnsafeContentType() {
result =
[
"text/html", "application/xhtml+xml", "application/xml", "text/xml", "image/svg+xml",
"text/xsl", "application/vnd.wap.xhtml+xml", "text/rdf", "application/rdf+xml",
"application/mathml+xml", "text/vtt", "text/cache-manifest"
]
}
/**
* Holds if `h` may send a response with a content type that is safe for XSS.
*/
Http::HeaderDefinition xssSafeContentTypeHeader(Http::RouteHandler h) {
result = h.getAResponseHeader("content-type") and
not exists(string tp | result.defines("content-type", tp) |
tp.toLowerCase().matches(xssUnsafeContentType() + "%")
)
}
/**
* 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.getAHeaderName() = "content-type" and
dominatingHeader.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
)
)
}
bindingset[headerBlock]
pragma[inline_late]
private predicate doesNotDominateCallback(ReachableBasicBlock headerBlock) {
not exists(Expr e | e instanceof Function | headerBlock.dominates(e.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.
doesNotDominateCallback(headerBlock)
)
}
/**
* 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 { }
/** A third-party controllable request input, considered as a flow source for reflected XSS. */
class ThirdPartyRequestInputAccessAsSource extends Source {
ThirdPartyRequestInputAccessAsSource() {
this.(Http::RequestInputAccess).isThirdPartyControllable()
or
this.(Http::RequestHeaderAccess).getAHeaderName() = "referer"
}
}
private class SinkFromModel extends Sink {
SinkFromModel() { this = ModelOutput::getASinkNode("html-injection").asSink() }
}
}