Skip to content

Commit 4567df0

Browse files
Merge pull request #200 from advanced-security/jeongsoolee09/model-cds-shortcuts
Improve CQL Injection Query
2 parents 18d9fb7 + 6bfec53 commit 4567df0

File tree

19 files changed

+3270
-348
lines changed

19 files changed

+3270
-348
lines changed

.github/workflows/javascript.sarif.expected

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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

Comments
 (0)