mirror of
https://github.com/github/codeql.git
synced 2026-01-28 13:53:10 +01:00
220 lines
6.7 KiB
Plaintext
220 lines
6.7 KiB
Plaintext
/**
|
|
* Provides default sources, sinks and sanitizers for reasoning about
|
|
* unsafe jQuery plugins, as well as extension points for adding your
|
|
* own.
|
|
*/
|
|
|
|
import javascript
|
|
private import semmle.javascript.dataflow.InferredTypes
|
|
import semmle.javascript.security.dataflow.Xss
|
|
|
|
module UnsafeJQueryPlugin {
|
|
private import DataFlow::FlowLabel
|
|
|
|
/**
|
|
* A data flow source for unsafe jQuery plugins.
|
|
*/
|
|
abstract class Source extends DataFlow::Node {
|
|
/**
|
|
* Gets the plugin that this source is used in.
|
|
*/
|
|
abstract JQuery::JQueryPluginMethod getPlugin();
|
|
}
|
|
|
|
/**
|
|
* A data flow sink for unsafe jQuery plugins.
|
|
*/
|
|
abstract class Sink extends DataFlow::Node { }
|
|
|
|
/**
|
|
* A sanitizer for unsafe jQuery plugins.
|
|
*/
|
|
abstract class Sanitizer extends DataFlow::Node { }
|
|
|
|
/**
|
|
* An argument that may act as a HTML fragment rather than a CSS selector, as a sink for remote unsafe jQuery plugins.
|
|
*/
|
|
class AmbiguousHtmlOrSelectorArgument extends DataFlow::Node,
|
|
DomBasedXss::JQueryHtmlOrSelectorArgument {
|
|
AmbiguousHtmlOrSelectorArgument() {
|
|
// any fixed prefix makes the call unambiguous
|
|
not exists(getAPrefix())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets an operand to `extend`.
|
|
*/
|
|
private DataFlow::SourceNode getAnExtendOperand(DataFlow::TypeBackTracker t, ExtendCall extend) {
|
|
t.start() and
|
|
result.flowsTo(extend.getAnOperand())
|
|
or
|
|
exists(DataFlow::TypeBackTracker t2 | result = getAnExtendOperand(t2, extend).backtrack(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets an operand to `extend`.
|
|
*/
|
|
private DataFlow::SourceNode getAnExtendOperand(ExtendCall extend) {
|
|
result = getAnExtendOperand(DataFlow::TypeBackTracker::end(), extend)
|
|
}
|
|
|
|
/**
|
|
* Holds if `plugin` has a default option defined at `def`.
|
|
*/
|
|
private predicate hasDefaultOption(JQuery::JQueryPluginMethod plugin, DataFlow::PropWrite def) {
|
|
exists(ExtendCall extend, JQueryPluginOptions options, DataFlow::SourceNode default |
|
|
options.getPlugin() = plugin and
|
|
options = getAnExtendOperand(extend) and
|
|
default = getAnExtendOperand(extend) and
|
|
default.getAPropertyWrite() = def
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The client-provided options object for a jQuery plugin.
|
|
*/
|
|
class JQueryPluginOptions extends DataFlow::ParameterNode {
|
|
JQuery::JQueryPluginMethod method;
|
|
|
|
JQueryPluginOptions() {
|
|
exists(string optionsPattern |
|
|
optionsPattern = "(?i)(opt(ion)?s?)" and
|
|
if method.getAParameter().getName().regexpMatch(optionsPattern)
|
|
then (
|
|
// use the last parameter named something like "options" if it exists ...
|
|
getName().regexpMatch(optionsPattern) and
|
|
this = method.getAParameter()
|
|
) else (
|
|
// ... otherwise, use the last parameter, unless it looks like a DOM node
|
|
this = method.getLastParameter() and
|
|
not getName().regexpMatch("(?i)(e(l(em(ent(s)?)?)?)?)")
|
|
)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the plugin method that these options are used in.
|
|
*/
|
|
JQuery::JQueryPluginMethod getPlugin() { result = method }
|
|
}
|
|
|
|
/**
|
|
* An expression of form `isElement(x)`, which sanitizes `x`.
|
|
*/
|
|
class IsElementSanitizer extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode {
|
|
IsElementSanitizer() {
|
|
// common ad hoc sanitizing calls
|
|
exists(string name | getCalleeName() = name |
|
|
name = "isElement" or name = "isDocument" or name = "isWindow"
|
|
)
|
|
}
|
|
|
|
override predicate sanitizes(boolean outcome, Expr e) {
|
|
outcome = true and e = getArgument(0).asExpr()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An expression like `typeof x.<?> !== "undefined"` or `x.<?>`, which sanitizes `x`, as it is unlikely to be a string afterwards.
|
|
*/
|
|
class PropertyPresenceSanitizer extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode {
|
|
DataFlow::Node input;
|
|
boolean polarity;
|
|
|
|
PropertyPresenceSanitizer() {
|
|
exists(DataFlow::PropRead read, string name |
|
|
not name = "length" and read.accesses(input, name)
|
|
|
|
|
exists(EqualityTest test |
|
|
polarity = test.getPolarity().booleanNot() and
|
|
this = test.flow()
|
|
|
|
|
exists(Expr undef | test.hasOperands(read.asExpr(), undef) |
|
|
SyntacticConstants::isUndefined(undef)
|
|
)
|
|
or
|
|
TaintTracking::isTypeofGuard(test, read.asExpr(), "undefined")
|
|
)
|
|
or
|
|
polarity = true and
|
|
this = read
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the property read that is used to sanitize the base value.
|
|
*/
|
|
DataFlow::PropRead getPropRead() { result = this }
|
|
|
|
override predicate sanitizes(boolean outcome, Expr e) {
|
|
outcome = polarity and
|
|
e = input.asExpr()
|
|
}
|
|
}
|
|
|
|
/** A guard that checks whether `x` is a number. */
|
|
class NumberGuard extends TaintTracking::SanitizerGuardNode instanceof DataFlow::CallNode {
|
|
Expr x;
|
|
boolean polarity;
|
|
|
|
NumberGuard() { TaintTracking::isNumberGuard(this, x, polarity) }
|
|
|
|
override predicate sanitizes(boolean outcome, Expr e) { e = x and outcome = polarity }
|
|
}
|
|
|
|
/**
|
|
* The client-provided options object for a jQuery plugin, considered as a source for unsafe jQuery plugins.
|
|
*/
|
|
class JQueryPluginOptionsAsSource extends Source, JQueryPluginOptions {
|
|
override JQuery::JQueryPluginMethod getPlugin() {
|
|
result = JQueryPluginOptions.super.getPlugin()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An argument that may act as a HTML fragment rather than a CSS selector, as a sink for remote unsafe jQuery plugins.
|
|
*/
|
|
class AmbiguousHtmlOrSelectorArgumentAsSink extends Sink {
|
|
AmbiguousHtmlOrSelectorArgumentAsSink() {
|
|
this instanceof AmbiguousHtmlOrSelectorArgument and not isLikelyIntentionalHtmlSink(this)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A hint that a value is expected to be treated as a HTML fragment later.
|
|
*/
|
|
class IntentionalHtmlFragmentHint extends Sanitizer {
|
|
IntentionalHtmlFragmentHint() {
|
|
this.(DataFlow::PropRead).getPropertyName().regexpMatch("(?i).*(html|template).*")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Holds if there exists a jQuery plugin that likely expects `sink` to be treated as a HTML fragment.
|
|
*/
|
|
predicate isLikelyIntentionalHtmlSink(DataFlow::Node sink) {
|
|
exists(
|
|
JQuery::JQueryPluginMethod plugin, DataFlow::PropWrite defaultDef,
|
|
DataFlow::PropRead finalRead
|
|
|
|
|
hasDefaultOption(plugin, defaultDef) and
|
|
defaultDef = getALikelyHTMLWrite(finalRead.getPropertyName()) and
|
|
finalRead.flowsTo(sink) and
|
|
sink.getTopLevel() = plugin.getTopLevel()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a property-write that writes a HTML-like constant string to `prop`.
|
|
*/
|
|
pragma[noinline]
|
|
private DataFlow::PropWrite getALikelyHTMLWrite(string prop) {
|
|
exists(string default |
|
|
result.getRhs().mayHaveStringValue(default) and
|
|
default.regexpMatch("\\s*<.*") and
|
|
result.getPropertyName() = prop
|
|
)
|
|
}
|
|
}
|