mirror of
https://github.com/github/codeql.git
synced 2026-04-25 08:45:14 +02:00
JS: add query for incomplete HTML attribute sanitization
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @name Incomplete HTML attribute sanitization
|
||||
* @description Writing incompletely sanitized values to HTML
|
||||
* attribute strings can lead to a cross-site
|
||||
* scripting vulnerability.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @precision high
|
||||
* @id js/incomplete-html-attribute-sanitization
|
||||
* @tags security
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
* external/cwe/cwe-20
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import semmle.javascript.security.dataflow.IncompleteHtmlAttributeSanitization::IncompleteHtmlAttributeSanitization
|
||||
import semmle.javascript.security.IncompleteBlacklistSanitizer
|
||||
|
||||
/**
|
||||
* Gets a pretty string of the dangerous characters for `sink`.
|
||||
*/
|
||||
string prettyPrintDangerousCharaters(Sink sink) {
|
||||
result =
|
||||
strictconcat(string s |
|
||||
s = describeCharacters(sink.getADangerousCharacter())
|
||||
|
|
||||
s, ", " order by s
|
||||
).regexpReplaceAll(",(?=[^,]+$)", " or")
|
||||
}
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
// this message is slightly sub-optimal as we do not have an easy way
|
||||
// to get the flow labels that reach the sink, so the message includes
|
||||
// all of them in a disjunction
|
||||
"Cross-site scripting vulnerability as the output of $@ may contain " +
|
||||
prettyPrintDangerousCharaters(sink.getNode()) + " when it reaches this attribute definition.",
|
||||
source.getNode(), "this final HTML sanitizer step"
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Provides classes and predicates for working with incomplete blacklist sanitizers.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* An incomplete black-list sanitizer.
|
||||
*/
|
||||
abstract class IncompleteBlacklistSanitizer extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a relevant character that is not sanitized by this sanitizer.
|
||||
*/
|
||||
abstract string getAnUnsanitizedCharacter();
|
||||
|
||||
/**
|
||||
* Gets the kind of sanitization this sanitizer performs.
|
||||
*/
|
||||
abstract string getKind();
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the characters represented by `rep`.
|
||||
*/
|
||||
string describeCharacters(string rep) {
|
||||
rep = "\"" and result = "quotes"
|
||||
or
|
||||
rep = "&" and result = "ampersands"
|
||||
or
|
||||
rep = "<" and result = "less-thans"
|
||||
or
|
||||
rep = ">" and result = "greater-thans"
|
||||
}
|
||||
|
||||
/**
|
||||
* A local sequence of calls to `String.prototype.replace`,
|
||||
* represented by the last call.
|
||||
*/
|
||||
class StringReplaceCallSequence extends DataFlow::CallNode {
|
||||
StringReplaceCallSequence() {
|
||||
this instanceof StringReplaceCall and
|
||||
not exists(getAStringReplaceMethodCall(this)) // terminal
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member of this sequence.
|
||||
*/
|
||||
StringReplaceCall getAMember() { this = getAStringReplaceMethodCall*(result) }
|
||||
|
||||
/** Gets a string that is the replacement of this call. */
|
||||
string getAReplacementString() {
|
||||
// this is more restrictive than `StringReplaceCall::replaces/2`, in the name of precision
|
||||
getAMember().getRawReplacement().getStringValue() = result
|
||||
}
|
||||
|
||||
/** Gets a string that is being replaced by this call. */
|
||||
string getAReplacedString() { getAMember().getAReplacedString() = result }
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialized version of `DataFlow::Node::getAMethodCall` that is
|
||||
* restricted to `StringReplaceCall`-nodes.
|
||||
*/
|
||||
private StringReplaceCall getAStringReplaceMethodCall(StringReplaceCall n) {
|
||||
n.getAMethodCall() = result
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides predicates and classes for reasoning about HTML sanitization.
|
||||
*/
|
||||
module HtmlSanitization {
|
||||
private predicate fixedGlobalReplacement(StringReplaceCallSequence chain) {
|
||||
forall(StringReplaceCall member | member = chain.getAMember() |
|
||||
member.isGlobal() and member.getArgument(0) instanceof DataFlow::RegExpLiteralNode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a HTML-relevant character that is replaced by `chain`.
|
||||
*/
|
||||
private string getALikelyReplacedCharacter(StringReplaceCallSequence chain) {
|
||||
result = "\"" and
|
||||
(
|
||||
chain.getAReplacedString() = result or
|
||||
chain.getAReplacementString() = """ or
|
||||
chain.getAReplacementString() = """
|
||||
)
|
||||
or
|
||||
result = "&" and
|
||||
(
|
||||
chain.getAReplacedString() = result or
|
||||
chain.getAReplacementString() = "&" or
|
||||
chain.getAReplacementString() = "("
|
||||
)
|
||||
or
|
||||
result = "<" and
|
||||
(
|
||||
chain.getAReplacedString() = result or
|
||||
chain.getAReplacementString() = "<" or
|
||||
chain.getAReplacementString() = "<"
|
||||
)
|
||||
or
|
||||
result = ">" and
|
||||
(
|
||||
chain.getAReplacedString() = result or
|
||||
chain.getAReplacementString() = ">" or
|
||||
chain.getAReplacementString() = ">"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An incomplete sanitizer for HTML-relevant characters.
|
||||
*/
|
||||
class IncompleteSanitizer extends IncompleteBlacklistSanitizer {
|
||||
StringReplaceCallSequence chain;
|
||||
string unsanitized;
|
||||
|
||||
IncompleteSanitizer() {
|
||||
this = chain and
|
||||
fixedGlobalReplacement(chain) and
|
||||
not getALikelyReplacedCharacter(chain) = unsanitized and
|
||||
(
|
||||
// replaces `<` and `>`
|
||||
getALikelyReplacedCharacter(chain) = "<" and
|
||||
getALikelyReplacedCharacter(chain) = ">" and
|
||||
(
|
||||
unsanitized = "\""
|
||||
or
|
||||
unsanitized = "&"
|
||||
)
|
||||
or
|
||||
// replaces '&' and either `<` or `>`
|
||||
getALikelyReplacedCharacter(chain) = "&" and
|
||||
(
|
||||
getALikelyReplacedCharacter(chain) = ">" and
|
||||
unsanitized = ">"
|
||||
or
|
||||
getALikelyReplacedCharacter(chain) = "<" and
|
||||
unsanitized = "<"
|
||||
)
|
||||
) and
|
||||
// does not replace special characters that the browser doesn't care for
|
||||
not chain.getAReplacedString() = ["!", "#", "*", "?", "@", "|", "~"] and
|
||||
/// only replaces explicit characters: exclude character ranges and negated character classes
|
||||
not exists(RegExpTerm t | t = chain.getAMember().getRegExp().getRoot().getAChild*() |
|
||||
t.(RegExpCharacterClass).isInverted() or
|
||||
t instanceof RegExpCharacterRange
|
||||
) and
|
||||
// the replacements are either empty or HTML entities
|
||||
chain.getAReplacementString().regexpMatch("(?i)(|(&[#a-z0-9]+;))")
|
||||
}
|
||||
|
||||
override string getKind() { result = "HTML" }
|
||||
|
||||
override string getAnUnsanitizedCharacter() { result = unsanitized }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Provides a taint tracking configuration for reasoning about
|
||||
* incomplete HTML sanitization vulnerabilities.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `IncompleteHtmlAttributeSanitization::Configuration` is needed, otherwise
|
||||
* `IncompleteHtmlAttributeSanitizationCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
module IncompleteHtmlAttributeSanitization {
|
||||
import IncompleteHtmlAttributeSanitizationCustomizations::IncompleteHtmlAttributeSanitization
|
||||
|
||||
private module Label {
|
||||
class Quote extends DataFlow::FlowLabel {
|
||||
Quote() { this = "\"" }
|
||||
}
|
||||
|
||||
class Ampersand extends DataFlow::FlowLabel {
|
||||
Ampersand() { this = "&" }
|
||||
}
|
||||
|
||||
DataFlow::FlowLabel characterToLabel(string c) { c = result }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "IncompleteHtmlAttributeSanitization" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
|
||||
label = Label::characterToLabel(source.(Source).getAnUnsanitizedCharacter())
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
|
||||
label = Label::characterToLabel(sink.(Sink).getADangerousCharacter())
|
||||
}
|
||||
|
||||
override predicate isAdditionalFlowStep(
|
||||
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel,
|
||||
DataFlow::FlowLabel dstlabel
|
||||
) {
|
||||
super.isAdditionalFlowStep(src, dst) and srclabel = dstlabel
|
||||
}
|
||||
|
||||
override predicate isLabeledBarrier(DataFlow::Node node, DataFlow::FlowLabel lbl) {
|
||||
lbl = Label::characterToLabel(node.(StringReplaceCall).getAReplacedString()) or
|
||||
isSanitizer(node)
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node n) {
|
||||
n instanceof Sanitizer or
|
||||
super.isSanitizer(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for reasoning about
|
||||
* incomplete HTML sanitization vulnerabilities, as well as extension
|
||||
* points for adding your own.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.RemoteFlowSources
|
||||
import semmle.javascript.security.IncompleteBlacklistSanitizer
|
||||
|
||||
module IncompleteHtmlAttributeSanitization {
|
||||
/**
|
||||
* A data flow source for incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a character that may come out of this source.
|
||||
*/
|
||||
abstract string getAnUnsanitizedCharacter();
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow sink for incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a character that is dangerous for this sink.
|
||||
*/
|
||||
abstract string getADangerousCharacter();
|
||||
}
|
||||
|
||||
/**
|
||||
* A sanitizer for incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A source of incompletely sanitized characters, considered as a
|
||||
* flow source for incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
class IncompleteHtmlSanitizerAsSource extends Source, HtmlSanitization::IncompleteSanitizer {
|
||||
override string getAnUnsanitizedCharacter() {
|
||||
result = HtmlSanitization::IncompleteSanitizer.super.getAnUnsanitizedCharacter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A concatenation that syntactically looks like a definition of an HTML attribute.
|
||||
*/
|
||||
class HtmlAttributeConcatenation extends StringOps::ConcatenationLeaf {
|
||||
string lhs;
|
||||
|
||||
HtmlAttributeConcatenation() {
|
||||
lhs = this.getPreviousLeaf().getStringValue().regexpCapture("(.*)=\"[^\"]*", 1) and
|
||||
this.getNextLeaf().getStringValue().regexpMatch(".*\".*")
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the attribute value is interpreted as JavaScript source code.
|
||||
*/
|
||||
predicate isInterpretedAsJavaScript() { lhs.regexpMatch("(?i)(.* )?on[a-z]+") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A concatenation that syntactically looks like a definition of an
|
||||
* HTML attribute, as a sink for incomplete HTML sanitization
|
||||
* vulnerabilities.
|
||||
*/
|
||||
class HtmlAttributeConcatenationAsSink extends Sink, DataFlow::ValueNode,
|
||||
HtmlAttributeConcatenation {
|
||||
override string getADangerousCharacter() {
|
||||
isInterpretedAsJavaScript() and result = "&"
|
||||
or
|
||||
result = "\""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An encoder for potentially malicious characters, as a sanitizer
|
||||
* for incomplete HTML sanitization vulnerabilities.
|
||||
*/
|
||||
class EncodingSanitizer extends Sanitizer {
|
||||
EncodingSanitizer() {
|
||||
this = DataFlow::globalVarRef(["encodeURIComponent", "encodeURI"]).getACall()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user