|
| 1 | +private import codeql.ruby.Concepts |
| 2 | +private import codeql.ruby.AST |
| 3 | +private import codeql.ruby.DataFlow |
| 4 | +private import codeql.ruby.typetracking.TypeTracker |
| 5 | +private import codeql.ruby.ApiGraphs |
| 6 | +private import codeql.ruby.controlflow.CfgNodes as CfgNodes |
| 7 | + |
| 8 | +private class NokogiriXmlParserCall extends XmlParserCall::Range, DataFlow::CallNode { |
| 9 | + NokogiriXmlParserCall() { |
| 10 | + this = |
| 11 | + [ |
| 12 | + API::getTopLevelMember("Nokogiri").getMember("XML"), |
| 13 | + API::getTopLevelMember("Nokogiri").getMember("XML").getMember("Document"), |
| 14 | + API::getTopLevelMember("Nokogiri") |
| 15 | + .getMember("XML") |
| 16 | + .getMember("SAX") |
| 17 | + .getMember("Parser") |
| 18 | + .getInstance() |
| 19 | + ].getAMethodCall("parse") |
| 20 | + } |
| 21 | + |
| 22 | + override DataFlow::Node getInput() { result = this.getArgument(0) } |
| 23 | + |
| 24 | + override predicate externalEntitiesEnabled() { |
| 25 | + this.getArgument(3) = |
| 26 | + [trackEnableFeature(TNOENT()), trackEnableFeature(TDTDLOAD()), trackDisableFeature(TNONET())] |
| 27 | + or |
| 28 | + // calls to methods that enable/disable features in a block argument passed to this parser call. |
| 29 | + // For example: |
| 30 | + // ```ruby |
| 31 | + // doc.parse(...) { |options| options.nononet; options.noent } |
| 32 | + // ``` |
| 33 | + this.asExpr() |
| 34 | + .getExpr() |
| 35 | + .(MethodCall) |
| 36 | + .getBlock() |
| 37 | + .getAStmt() |
| 38 | + .getAChild*() |
| 39 | + .(MethodCall) |
| 40 | + .getMethodName() = ["noent", "dtdload", "nononet"] |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +private class LibXmlRubyXmlParserCall extends XmlParserCall::Range, DataFlow::CallNode { |
| 45 | + LibXmlRubyXmlParserCall() { |
| 46 | + this = |
| 47 | + [API::getTopLevelMember("LibXML").getMember("XML"), API::getTopLevelMember("XML")] |
| 48 | + .getMember(["Document", "Parser"]) |
| 49 | + .getAMethodCall(["file", "io", "string"]) |
| 50 | + } |
| 51 | + |
| 52 | + override DataFlow::Node getInput() { result = this.getArgument(0) } |
| 53 | + |
| 54 | + override predicate externalEntitiesEnabled() { |
| 55 | + exists(Pair pair | |
| 56 | + pair = this.getArgument(1).asExpr().getExpr().(HashLiteral).getAKeyValuePair() and |
| 57 | + pair.getKey().(Literal).getValueText() = "options" and |
| 58 | + pair.getValue() = |
| 59 | + [ |
| 60 | + trackEnableFeature(TNOENT()), trackEnableFeature(TDTDLOAD()), |
| 61 | + trackDisableFeature(TNONET()) |
| 62 | + ].asExpr().getExpr() |
| 63 | + ) |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +private newtype TFeature = |
| 68 | + TNOENT() or |
| 69 | + TNONET() or |
| 70 | + TDTDLOAD() |
| 71 | + |
| 72 | +class Feature extends TFeature { |
| 73 | + abstract int getValue(); |
| 74 | + |
| 75 | + string toString() { result = getConstantName() } |
| 76 | + |
| 77 | + abstract string getConstantName(); |
| 78 | +} |
| 79 | + |
| 80 | +private class FeatureNOENT extends Feature, TNOENT { |
| 81 | + override int getValue() { result = 2 } |
| 82 | + |
| 83 | + override string getConstantName() { result = "NOENT" } |
| 84 | +} |
| 85 | + |
| 86 | +private class FeatureNONET extends Feature, TNONET { |
| 87 | + override int getValue() { result = 2048 } |
| 88 | + |
| 89 | + override string getConstantName() { result = "NONET" } |
| 90 | +} |
| 91 | + |
| 92 | +private class FeatureDTDLOAD extends Feature, TDTDLOAD { |
| 93 | + override int getValue() { result = 4 } |
| 94 | + |
| 95 | + override string getConstantName() { result = "DTDLOAD" } |
| 96 | +} |
| 97 | + |
| 98 | +private API::Node parseOptionsModule() { |
| 99 | + result = API::getTopLevelMember("Nokogiri").getMember("XML").getMember("ParseOptions") |
| 100 | + or |
| 101 | + result = |
| 102 | + API::getTopLevelMember("LibXML").getMember("XML").getMember("Parser").getMember("Options") |
| 103 | + or |
| 104 | + result = API::getTopLevelMember("XML").getMember("Parser").getMember("Options") |
| 105 | +} |
| 106 | + |
| 107 | +private predicate bitWiseAndOr(CfgNodes::ExprNodes::OperationCfgNode operation) { |
| 108 | + operation.getExpr() instanceof BitwiseAndExpr or |
| 109 | + operation.getExpr() instanceof AssignBitwiseAndExpr or |
| 110 | + operation.getExpr() instanceof BitwiseOrExpr or |
| 111 | + operation.getExpr() instanceof AssignBitwiseOrExpr |
| 112 | +} |
| 113 | + |
| 114 | +private DataFlow::LocalSourceNode trackFeature(Feature f, boolean enable, TypeTracker t) { |
| 115 | + t.start() and |
| 116 | + ( |
| 117 | + // An integer literal with the feature-bit enabled/disabled |
| 118 | + exists(int bitValue | |
| 119 | + bitValue = result.asExpr().getExpr().(IntegerLiteral).getValue().bitAnd(f.getValue()) |
| 120 | + | |
| 121 | + if bitValue = 0 then enable = false else enable = true |
| 122 | + ) |
| 123 | + or |
| 124 | + // Use of a constant f |
| 125 | + enable = true and |
| 126 | + result = parseOptionsModule().getMember(f.getConstantName()).getAUse() |
| 127 | + or |
| 128 | + // Treat `&`, `&=`, `|` and `|=` operators as if they preserve the on/off states |
| 129 | + // of their operands. This is an overapproximation but likely to work well in practice |
| 130 | + // because it makes little sense to explicitly set a feature to both `on` and `off` in the |
| 131 | + // same code. |
| 132 | + exists(CfgNodes::ExprNodes::OperationCfgNode operation | |
| 133 | + bitWiseAndOr(operation) and |
| 134 | + operation = result.asExpr().(CfgNodes::ExprNodes::OperationCfgNode) and |
| 135 | + operation.getAnOperand() = trackFeature(f, enable).asExpr() |
| 136 | + ) |
| 137 | + or |
| 138 | + // The complement operator toggles a feature from enabled to disabled and vice-versa |
| 139 | + result.asExpr().getExpr() instanceof ComplementExpr and |
| 140 | + result.asExpr().(CfgNodes::ExprNodes::OperationCfgNode).getAnOperand() = |
| 141 | + trackFeature(f, enable.booleanNot()).asExpr() |
| 142 | + or |
| 143 | + // Nokogiri has a ParseOptions class that is a wrapper around the bit-fields and |
| 144 | + // provides methods for querying and updating the fields. |
| 145 | + result = |
| 146 | + API::getTopLevelMember("Nokogiri") |
| 147 | + .getMember("XML") |
| 148 | + .getMember("ParseOptions") |
| 149 | + .getAnInstantiation() and |
| 150 | + result.asExpr().(CfgNodes::ExprNodes::CallCfgNode).getArgument(0) = |
| 151 | + trackFeature(f, enable).asExpr() |
| 152 | + or |
| 153 | + // The Nokogiri ParseOptions class has methods for setting/unsetting features. |
| 154 | + // The method names are the lowercase variants of the constant names, with a "no" |
| 155 | + // prefix for unsetting a feature. |
| 156 | + exists(CfgNodes::ExprNodes::CallCfgNode call | |
| 157 | + enable = true and |
| 158 | + call.getExpr().(MethodCall).getMethodName() = f.getConstantName().toLowerCase() |
| 159 | + or |
| 160 | + enable = false and |
| 161 | + call.getExpr().(MethodCall).getMethodName() = "no" + f.getConstantName().toLowerCase() |
| 162 | + | |
| 163 | + ( |
| 164 | + // these methods update the receiver |
| 165 | + result.flowsTo(any(DataFlow::Node n | n.asExpr() = call.getReceiver())) |
| 166 | + or |
| 167 | + // in addition they return the (updated) receiver to allow chaining calls. |
| 168 | + result.asExpr() = call |
| 169 | + ) |
| 170 | + ) |
| 171 | + ) |
| 172 | + or |
| 173 | + exists(TypeTracker t2 | result = trackFeature(f, enable, t2).track(t2, t)) |
| 174 | +} |
| 175 | + |
| 176 | +private DataFlow::Node trackFeature(Feature f, boolean enable) { |
| 177 | + trackFeature(f, enable, TypeTracker::end()).flowsTo(result) |
| 178 | +} |
| 179 | + |
| 180 | +private DataFlow::Node trackEnableFeature(Feature f) { result = trackFeature(f, true) } |
| 181 | + |
| 182 | +private DataFlow::Node trackDisableFeature(Feature f) { result = trackFeature(f, false) } |
0 commit comments