Merge pull request #286 from github/aibaars/xxe

XXE query
This commit is contained in:
Arthur Baars
2021-10-01 16:14:41 +02:00
committed by GitHub
11 changed files with 462 additions and 0 deletions

View File

@@ -503,3 +503,38 @@ module CodeExecution {
abstract DataFlow::Node getCode();
}
}
/**
* A data-flow node that parses XML content.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `XmlParserCall::Range` instead.
*/
class XmlParserCall extends DataFlow::Node {
XmlParserCall::Range range;
XmlParserCall() { this = range }
/** Gets the argument that specifies the XML content to be parsed. */
DataFlow::Node getInput() { result = range.getInput() }
/** Holds if this XML parser call is configured to process external entities */
predicate externalEntitiesEnabled() { range.externalEntitiesEnabled() }
}
/** Provides a class for modeling new XML parsing APIs. */
module XmlParserCall {
/**
* A data-flow node that parses XML content.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `class XmlParserCall` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the XML content to be parsed. */
abstract DataFlow::Node getInput();
/** Holds if this XML parser call is configured to process external entities */
abstract predicate externalEntitiesEnabled();
}
}

View File

@@ -8,3 +8,4 @@ private import codeql.ruby.frameworks.ActionView
private import codeql.ruby.frameworks.StandardLibrary
private import codeql.ruby.frameworks.Files
private import codeql.ruby.frameworks.HttpClients
private import codeql.ruby.frameworks.XmlParsing

View File

@@ -0,0 +1,182 @@
private import codeql.ruby.Concepts
private import codeql.ruby.AST
private import codeql.ruby.DataFlow
private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.ApiGraphs
private import codeql.ruby.controlflow.CfgNodes as CfgNodes
private class NokogiriXmlParserCall extends XmlParserCall::Range, DataFlow::CallNode {
NokogiriXmlParserCall() {
this =
[
API::getTopLevelMember("Nokogiri").getMember("XML"),
API::getTopLevelMember("Nokogiri").getMember("XML").getMember("Document"),
API::getTopLevelMember("Nokogiri")
.getMember("XML")
.getMember("SAX")
.getMember("Parser")
.getInstance()
].getAMethodCall("parse")
}
override DataFlow::Node getInput() { result = this.getArgument(0) }
override predicate externalEntitiesEnabled() {
this.getArgument(3) =
[trackEnableFeature(TNOENT()), trackEnableFeature(TDTDLOAD()), trackDisableFeature(TNONET())]
or
// calls to methods that enable/disable features in a block argument passed to this parser call.
// For example:
// ```ruby
// doc.parse(...) { |options| options.nononet; options.noent }
// ```
this.asExpr()
.getExpr()
.(MethodCall)
.getBlock()
.getAStmt()
.getAChild*()
.(MethodCall)
.getMethodName() = ["noent", "dtdload", "nononet"]
}
}
private class LibXmlRubyXmlParserCall extends XmlParserCall::Range, DataFlow::CallNode {
LibXmlRubyXmlParserCall() {
this =
[API::getTopLevelMember("LibXML").getMember("XML"), API::getTopLevelMember("XML")]
.getMember(["Document", "Parser"])
.getAMethodCall(["file", "io", "string"])
}
override DataFlow::Node getInput() { result = this.getArgument(0) }
override predicate externalEntitiesEnabled() {
exists(Pair pair |
pair = this.getArgument(1).asExpr().getExpr().(HashLiteral).getAKeyValuePair() and
pair.getKey().(Literal).getValueText() = "options" and
pair.getValue() =
[
trackEnableFeature(TNOENT()), trackEnableFeature(TDTDLOAD()),
trackDisableFeature(TNONET())
].asExpr().getExpr()
)
}
}
private newtype TFeature =
TNOENT() or
TNONET() or
TDTDLOAD()
class Feature extends TFeature {
abstract int getValue();
string toString() { result = getConstantName() }
abstract string getConstantName();
}
private class FeatureNOENT extends Feature, TNOENT {
override int getValue() { result = 2 }
override string getConstantName() { result = "NOENT" }
}
private class FeatureNONET extends Feature, TNONET {
override int getValue() { result = 2048 }
override string getConstantName() { result = "NONET" }
}
private class FeatureDTDLOAD extends Feature, TDTDLOAD {
override int getValue() { result = 4 }
override string getConstantName() { result = "DTDLOAD" }
}
private API::Node parseOptionsModule() {
result = API::getTopLevelMember("Nokogiri").getMember("XML").getMember("ParseOptions")
or
result =
API::getTopLevelMember("LibXML").getMember("XML").getMember("Parser").getMember("Options")
or
result = API::getTopLevelMember("XML").getMember("Parser").getMember("Options")
}
private predicate bitWiseAndOr(CfgNodes::ExprNodes::OperationCfgNode operation) {
operation.getExpr() instanceof BitwiseAndExpr or
operation.getExpr() instanceof AssignBitwiseAndExpr or
operation.getExpr() instanceof BitwiseOrExpr or
operation.getExpr() instanceof AssignBitwiseOrExpr
}
private DataFlow::LocalSourceNode trackFeature(Feature f, boolean enable, TypeTracker t) {
t.start() and
(
// An integer literal with the feature-bit enabled/disabled
exists(int bitValue |
bitValue = result.asExpr().getExpr().(IntegerLiteral).getValue().bitAnd(f.getValue())
|
if bitValue = 0 then enable = false else enable = true
)
or
// Use of a constant f
enable = true and
result = parseOptionsModule().getMember(f.getConstantName()).getAUse()
or
// Treat `&`, `&=`, `|` and `|=` operators as if they preserve the on/off states
// of their operands. This is an overapproximation but likely to work well in practice
// because it makes little sense to explicitly set a feature to both `on` and `off` in the
// same code.
exists(CfgNodes::ExprNodes::OperationCfgNode operation |
bitWiseAndOr(operation) and
operation = result.asExpr().(CfgNodes::ExprNodes::OperationCfgNode) and
operation.getAnOperand() = trackFeature(f, enable).asExpr()
)
or
// The complement operator toggles a feature from enabled to disabled and vice-versa
result.asExpr().getExpr() instanceof ComplementExpr and
result.asExpr().(CfgNodes::ExprNodes::OperationCfgNode).getAnOperand() =
trackFeature(f, enable.booleanNot()).asExpr()
or
// Nokogiri has a ParseOptions class that is a wrapper around the bit-fields and
// provides methods for querying and updating the fields.
result =
API::getTopLevelMember("Nokogiri")
.getMember("XML")
.getMember("ParseOptions")
.getAnInstantiation() and
result.asExpr().(CfgNodes::ExprNodes::CallCfgNode).getArgument(0) =
trackFeature(f, enable).asExpr()
or
// The Nokogiri ParseOptions class has methods for setting/unsetting features.
// The method names are the lowercase variants of the constant names, with a "no"
// prefix for unsetting a feature.
exists(CfgNodes::ExprNodes::CallCfgNode call |
enable = true and
call.getExpr().(MethodCall).getMethodName() = f.getConstantName().toLowerCase()
or
enable = false and
call.getExpr().(MethodCall).getMethodName() = "no" + f.getConstantName().toLowerCase()
|
(
// these methods update the receiver
result.flowsTo(any(DataFlow::Node n | n.asExpr() = call.getReceiver()))
or
// in addition they return the (updated) receiver to allow chaining calls.
result.asExpr() = call
)
)
)
or
exists(TypeTracker t2 | result = trackFeature(f, enable, t2).track(t2, t))
}
private DataFlow::Node trackFeature(Feature f, boolean enable) {
trackFeature(f, enable, TypeTracker::end()).flowsTo(result)
}
private DataFlow::Node trackEnableFeature(Feature f) { result = trackFeature(f, true) }
private DataFlow::Node trackDisableFeature(Feature f) { result = trackFeature(f, false) }