|
| 1 | +import javascript |
| 2 | +import semmle.javascript.security.dataflow.SqlInjectionCustomizations |
| 3 | +import advanced_security.javascript.frameworks.cap.CQL |
| 4 | +import advanced_security.javascript.frameworks.cap.RemoteFlowSources |
| 5 | +import advanced_security.javascript.frameworks.cap.dataflow.FlowSteps |
| 6 | + |
| 7 | +abstract class CqlInjectionSink extends DataFlow::Node { |
| 8 | + /** |
| 9 | + * Gets the data flow node that represents the query being run, for |
| 10 | + * accurate reporting. |
| 11 | + */ |
| 12 | + abstract DataFlow::Node getQuery(); |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * A CQL clause parameterized with a string concatentation expression. |
| 17 | + */ |
| 18 | +class CqlClauseWithStringConcatParameter instanceof CqlClause { |
| 19 | + CqlClauseWithStringConcatParameter() { |
| 20 | + exists(DataFlow::Node queryParameter | |
| 21 | + queryParameter = this.getArgument().flow() and |
| 22 | + exists(StringConcatenation::getAnOperand(queryParameter)) |
| 23 | + ) |
| 24 | + } |
| 25 | + |
| 26 | + Location getLocation() { result = super.getLocation() } |
| 27 | + |
| 28 | + string toString() { result = super.toString() } |
| 29 | +} |
| 30 | + |
| 31 | +/** |
| 32 | + * An await expression that has as its operand a CQL clause that includes a |
| 33 | + * string concatenation operation. |
| 34 | + */ |
| 35 | +class AwaitCqlClauseWithStringConcatParameter extends CqlInjectionSink { |
| 36 | + DataFlow::Node queryParameter; |
| 37 | + DataFlow::Node query; |
| 38 | + CqlClauseWithStringConcatParameter cqlClauseWithStringConcat; |
| 39 | + CqlClause finalAncestorCqlClauseOfCqlClauseWithStringConcat; |
| 40 | + |
| 41 | + AwaitCqlClauseWithStringConcatParameter() { |
| 42 | + exists(AwaitExpr await | |
| 43 | + this = await.flow() and |
| 44 | + await.getOperand() = finalAncestorCqlClauseOfCqlClauseWithStringConcat.asExpr() and |
| 45 | + finalAncestorCqlClauseOfCqlClauseWithStringConcat = |
| 46 | + cqlClauseWithStringConcat.(CqlClause).getFinalClause() |
| 47 | + ) |
| 48 | + } |
| 49 | + |
| 50 | + override DataFlow::Node getQuery() { |
| 51 | + result = finalAncestorCqlClauseOfCqlClauseWithStringConcat.flow() |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +/** |
| 56 | + * The first argument passed to the call to `cds.run`, `cds.db.run`, or `srv.run` |
| 57 | + * whose value is a CQL query object that includes a string concatenation. e.g. |
| 58 | + * ``` javascript |
| 59 | + * // 1. CQN object constructed from Fluent API |
| 60 | + * const query = SELECT.from`Entity1`.where("ID=" + id); |
| 61 | + * cds.run(query); |
| 62 | + * |
| 63 | + * // 2. CQN object parsed from a string |
| 64 | + * const query = cds.parse.cql("SELECT * from Entity1 where ID =" + id); |
| 65 | + * cds.run(query); |
| 66 | + * |
| 67 | + * // 3. An unparsed CQL string (only valid in old versions of CAP) |
| 68 | + * const query = "SELECT * from Entity1 where ID =" + id; |
| 69 | + * Service2.run(query); |
| 70 | + * ``` |
| 71 | + * The `getQuery/0` member predicate gets the `query` argument of the above calls |
| 72 | + * to `run`. |
| 73 | + */ |
| 74 | +class StringConcatParameterOfCqlRunMethodQueryArgument extends CqlInjectionSink { |
| 75 | + CqlRunMethodCall cqlRunMethodCall; |
| 76 | + |
| 77 | + StringConcatParameterOfCqlRunMethodQueryArgument() { |
| 78 | + this = cqlRunMethodCall.getAQueryParameter() |
| 79 | + } |
| 80 | + |
| 81 | + override DataFlow::Node getQuery() { result = this } |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | + * A CQL shortcut method call (`read`, `create`, ...) parameterized with a string |
| 86 | + * concatenation expression. e.g. |
| 87 | + * ``` javascript |
| 88 | + * cds.read("Entity1").where(`ID=${id}`); // Notice the surrounding parentheses! |
| 89 | + * cds.update("Entity1").set("col1 = col1" + amount).where("col1 = " + id); |
| 90 | + * cds.delete("Entity1").where("ID =" + id); |
| 91 | + * ``` |
| 92 | + */ |
| 93 | +class CqlShortcutMethodCallWithStringConcat instanceof CqlShortcutMethodCall { |
| 94 | + DataFlow::Node stringConcatParameter; |
| 95 | + |
| 96 | + CqlShortcutMethodCallWithStringConcat() { |
| 97 | + stringConcatParameter = super.getAQueryParameter() and |
| 98 | + exists(StringConcatenation::getAnOperand(stringConcatParameter)) |
| 99 | + } |
| 100 | + |
| 101 | + Location getLocation() { result = super.getLocation() } |
| 102 | + |
| 103 | + string toString() { result = super.toString() } |
| 104 | + |
| 105 | + DataFlow::Node getStringConcatParameter() { result = stringConcatParameter } |
| 106 | +} |
| 107 | + |
| 108 | +/** |
| 109 | + * A string concatenation expression included in a CQL shortcut method call. e.g. |
| 110 | + * ``` javascript |
| 111 | + * cds.read("Entity1").where(`ID=${id}`); // Notice the surrounding parentheses! |
| 112 | + * cds.update("Entity1").set("col1 = col1" + amount).where("col1 = " + id); |
| 113 | + * cds.delete("Entity1").where("ID =" + id); |
| 114 | + * ``` |
| 115 | + * This class captures the string concatenation expressions appearing above: |
| 116 | + * 1. `ID=${id}` |
| 117 | + * 2. `"col1 = col1" + amount` |
| 118 | + * 3. `"col1 = " + id` |
| 119 | + * 4. `"ID =" + id` |
| 120 | + */ |
| 121 | +class StringConcatParameterOfCqlShortcutMethodCall extends CqlInjectionSink { |
| 122 | + CqlShortcutMethodCallWithStringConcat cqlShortcutMethodCallWithStringConcat; |
| 123 | + |
| 124 | + StringConcatParameterOfCqlShortcutMethodCall() { |
| 125 | + this = cqlShortcutMethodCallWithStringConcat.getStringConcatParameter() |
| 126 | + } |
| 127 | + |
| 128 | + override DataFlow::Node getQuery() { |
| 129 | + result = |
| 130 | + cqlShortcutMethodCallWithStringConcat.(CqlShortcutMethodCall).getFinalChainedMethodCall() |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +/** |
| 135 | + * A CQL parser call (`cds.ql`, `cds.parse.cql`, ...) parameterized with a string |
| 136 | + * conatenation expression. |
| 137 | + */ |
| 138 | +class CqlClauseParserCallWithStringConcat instanceof CqlClauseParserCall { |
| 139 | + CqlClauseParserCallWithStringConcat() { |
| 140 | + not this.getCdlString().(StringOps::Concatenation).asExpr() instanceof TemplateLiteral and |
| 141 | + exists(StringConcatenation::getAnOperand(this.getCdlString())) |
| 142 | + } |
| 143 | + |
| 144 | + Location getLocation() { result = super.getLocation() } |
| 145 | + |
| 146 | + string toString() { result = super.toString() } |
| 147 | +} |
| 148 | + |
| 149 | +/** |
| 150 | + * A data flow configuration from a remote flow source to a handful of sinks that run a CQL |
| 151 | + * query, either directly or indirectly by assembling one under the hood. |
| 152 | + * |
| 153 | + * The CQL injection happens if a fluent API builder (`SELECT`, `INSERT`, ...) or a |
| 154 | + * shortcut method call (`srv.read`, `srv.create`, ...) are called with a string |
| 155 | + * concatentation as one of its argument, which in practice can take one of its |
| 156 | + * following forms: |
| 157 | + * |
| 158 | + * 1. Concatentation with a string value with the `+` operator: |
| 159 | + * - Concatenation with a string: `"ID=" + expr` |
| 160 | + * - Concatenation with a template literal: `` `ID=` + expr `` |
| 161 | + * 2. Template literal that interpolates an expression in it but is not a tagged |
| 162 | + * template literal: `` SELECT.from`Entity`.where(`ID=${expr}`) `` |
| 163 | + * |
| 164 | + * The second case should be distinguished from the ones that have tagged template literals |
| 165 | + * for all of its builder calls: if the example were `` SELECT.from`Entity`.where`ID=${expr}` `` |
| 166 | + * instead (notice the lack of parentheses around the template literal), then the `where` call |
| 167 | + * becomes a parser call of the template literal following it and thus acts as a sanitizer. |
| 168 | + */ |
| 169 | +class CqlInjectionConfiguration extends TaintTracking::Configuration { |
| 170 | + CqlInjectionConfiguration() { this = "CQL injection from untrusted data" } |
| 171 | + |
| 172 | + override predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource } |
| 173 | + |
| 174 | + override predicate isSink(DataFlow::Node node) { node instanceof CqlInjectionSink } |
| 175 | + |
| 176 | + override predicate isSanitizer(DataFlow::Node node) { node instanceof SqlInjection::Sanitizer } |
| 177 | + |
| 178 | + override predicate isAdditionalTaintStep(DataFlow::Node start, DataFlow::Node end) { |
| 179 | + /* |
| 180 | + * 1. Given a call to a CQL parser, jump from the argument to the parser call itself. |
| 181 | + */ |
| 182 | + |
| 183 | + exists(CqlClauseParserCall cqlParserCall | |
| 184 | + start = cqlParserCall.getAnArgument() and |
| 185 | + end = cqlParserCall |
| 186 | + ) |
| 187 | + or |
| 188 | + /* |
| 189 | + * 2. Jump from a query parameter to the CQL query clause itself. e.g. Given below code: |
| 190 | + * |
| 191 | + * ``` javascript |
| 192 | + * await SELECT.from(Service1Entity).where("ID=" + id); |
| 193 | + * ``` |
| 194 | + * |
| 195 | + * This step jumps from `id` in the call to `where` to the entire SELECT clause. |
| 196 | + */ |
| 197 | + |
| 198 | + exists(CqlClause cqlClause | |
| 199 | + start = cqlClause.getArgument().flow().getAPredecessor*().(StringOps::Concatenation) and |
| 200 | + end = cqlClause.getFinalClause().flow() |
| 201 | + ) |
| 202 | + } |
| 203 | +} |
0 commit comments