Skip to content

Commit 25450a8

Browse files
committed
Finalize working draft on current CqlInjection test case
1 parent 5e39be5 commit 25450a8

File tree

4 files changed

+170
-62
lines changed

4 files changed

+170
-62
lines changed

javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPCqlInjectionQuery.qll

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,78 @@ import javascript
22
import semmle.javascript.security.dataflow.SqlInjectionCustomizations
33
import advanced_security.javascript.frameworks.cap.CQL
44
import advanced_security.javascript.frameworks.cap.RemoteFlowSources
5+
import advanced_security.javascript.frameworks.cap.dataflow.FlowSteps
56

6-
/**
7-
* A possibly tainted clause
8-
* any clause with a string concatenation in it
9-
* regardless of where that operand came from
10-
*/
11-
class TaintedClause instanceof CqlClause {
12-
TaintedClause() { exists(StringConcatenation::getAnOperand(this.getArgument().flow())) }
7+
class CqlClauseWithStringConcatParameter instanceof CqlClause {
8+
CqlClauseWithStringConcatParameter() {
9+
exists(DataFlow::Node queryParameter |
10+
(
11+
if this instanceof CqlInsertClause or this instanceof CqlUpsertClause
12+
then
13+
queryParameter = this.getArgument().flow() or
14+
queryParameter = this.getArgument().flow().(SourceNode).getAPropertyWrite().getRhs()
15+
else queryParameter = this.getArgument().flow()
16+
) and
17+
exists(StringConcatenation::getAnOperand(queryParameter))
18+
)
19+
}
20+
21+
Location getLocation() { result = super.getLocation() }
1322

1423
string toString() { result = super.toString() }
24+
}
1525

16-
Expr getArgument() { result = super.getArgument() }
26+
class CqlShortcutMethodCallWithStringConcat instanceof CqlShortcutMethodCall {
27+
CqlShortcutMethodCallWithStringConcat() {
28+
exists(StringConcatenation::getAnOperand(super.getAQueryParameter()))
29+
}
30+
31+
Location getLocation() { result = super.getLocation() }
1732

18-
Expr asExpr() { result = super.asExpr() }
33+
string toString() { result = super.toString() }
1934
}
2035

21-
/**
22-
* a more heurisitic based taint step
23-
* captures one of the alternative ways to construct query strings:
24-
* `cds.parse.cql(`string`+userInput)`
25-
* and considers them tainted if they've been concatenated against
26-
* in any manner
27-
*/
28-
class ParseCQLTaintedClause extends CallNode {
29-
ParseCQLTaintedClause() {
30-
this = any(CdsFacade cds).getMember("parse").getMember("cql").getACall() and
31-
exists(DataFlow::Node n |
32-
n = StringConcatenation::getAnOperand(this.getAnArgument()) and
33-
//omit the fact that the arg of cds.parse.cql (`SELECT * from Foo`)
34-
//is technically a string concat
35-
not n.asExpr() instanceof TemplateElement
36-
)
36+
class CqlClauseParserCallWithStringConcat instanceof CqlClauseParserCall {
37+
CqlClauseParserCallWithStringConcat() {
38+
exists(StringConcatenation::getAnOperand(super.getCdlString()))
3739
}
40+
41+
Location getLocation() { result = super.getLocation() }
42+
43+
string toString() { result = super.toString() }
3844
}
3945

40-
class CqlIConfiguration extends TaintTracking::Configuration {
41-
CqlIConfiguration() { this = "CqlInjection" }
46+
class CqlInjectionConfiguration extends TaintTracking::Configuration {
47+
CqlInjectionConfiguration() { this = "CqlInjection" }
4248

4349
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
4450

4551
override predicate isSink(DataFlow::Node node) {
46-
node = any(CdsFacade cds).getMember("db").getMember("run").getACall().getAnArgument()
52+
exists(CqlRunMethodCall cqlRunMethodCall |
53+
node = cqlRunMethodCall.(CqlRunMethodCall).getAQueryParameter()
54+
)
4755
or
48-
exists(AwaitExpr awaitExpr, CqlClause clause |
49-
node.asExpr() = clause.asExpr() and
50-
awaitExpr.getOperand() = clause.asExpr()
56+
exists(CqlShortcutMethodCallWithStringConcat queryRunnerCall |
57+
node = queryRunnerCall.(CqlQueryRunnerCall).getAQueryParameter()
58+
)
59+
or
60+
exists(AwaitExpr await, CqlClauseWithStringConcatParameter cqlClauseWithStringConcat |
61+
node = await.flow() and
62+
await.getOperand() = cqlClauseWithStringConcat.(CqlClause).getArgument()
5163
)
5264
}
5365

54-
override predicate isSanitizer(DataFlow::Node node) {
55-
super.isSanitizer(node) or
56-
node instanceof SqlInjection::Sanitizer
57-
}
66+
override predicate isSanitizer(DataFlow::Node node) { node instanceof SqlInjection::Sanitizer }
5867

59-
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
60-
//string concatenation in a clause arg taints the clause
61-
exists(TaintedClause clause |
62-
clause.getArgument() = pred.asExpr() and
63-
clause.asExpr() = succ.asExpr()
68+
override predicate isAdditionalTaintStep(DataFlow::Node start, DataFlow::Node end) {
69+
exists(CqlClauseParserCallWithStringConcat cqlParseCallWithStringConcat |
70+
start = cqlParseCallWithStringConcat.(CqlClauseParserCall).getAnArgument() and
71+
end = cqlParseCallWithStringConcat
6472
)
6573
or
66-
//less precise, any concat in the alternative sql stmt construction techniques
67-
exists(ParseCQLTaintedClause parse |
68-
parse.getAnArgument() = pred and
69-
parse = succ
74+
exists(CqlClause cqlClause |
75+
start = cqlClause.getArgument().flow() and
76+
end = cqlClause.flow()
7077
)
7178
}
7279
}

javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,31 @@ class ServiceInstanceFromCdsServe extends ServiceInstance {
146146
* const Service1 = cds.connect.to("service-2");
147147
* ```
148148
*/
149-
class ServiceInstanceFromCdsConnectTo extends ServiceInstance, SourceNode {
149+
class ServiceInstanceFromCdsConnectTo extends ServiceInstance, SourceNode instanceof PropRead {
150+
string serviceDesignator;
150151
string serviceName;
151152

152-
ServiceInstanceFromCdsConnectTo() { this = serviceInstanceFromCdsConnectTo(serviceName) }
153+
ServiceInstanceFromCdsConnectTo() {
154+
this = serviceInstanceFromCdsConnectTo(serviceDesignator).getAPropertyRead(serviceName)
155+
}
153156

154157
override UserDefinedApplicationService getDefinition() {
155158
/* 1. The service */
156159
exists(RequiredService serviceDecl |
157-
serviceDecl.getName() = serviceName and
160+
serviceDecl.getName() = [serviceName, serviceDesignator] and
158161
result.hasLocationInfo(serviceDecl.getImplementationFile().getAbsolutePath(), _, _, _, _)
159162
)
160163
or
161-
result.getUnqualifiedName() = serviceName
164+
result.getUnqualifiedName() = serviceDesignator
162165
}
163166

167+
string getServiceDesignator() { result = serviceDesignator }
168+
164169
string getServiceName() { result = serviceName }
165170
}
166171

167172
class DBServiceInstanceFromCdsConnectTo extends ServiceInstanceFromCdsConnectTo {
168-
DBServiceInstanceFromCdsConnectTo() { serviceName = "db" }
173+
DBServiceInstanceFromCdsConnectTo() { serviceDesignator = "db" }
169174
}
170175

171176
/**
@@ -639,15 +644,15 @@ abstract class EntityReference extends CdsReference {
639644
*/
640645
class EntityReferenceFromEntities extends EntityReference instanceof PropRead {
641646
/**
642-
* Property or call to entities
647+
* A read from property `entities` or a method call to `entities`.
643648
*/
644649
DataFlow::SourceNode entitiesAccess;
645650
/**
646-
* Receiver of the entities call or the base of propread
651+
* The receiver of the call to `entities` or the base of the read from `entities`.
647652
*/
648653
DataFlow::Node receiver;
649654
/**
650-
* Name of the (unqualified) entity being accessed
655+
* The unqualified name of the entity being accessed.
651656
*/
652657
string entityName;
653658

@@ -748,14 +753,16 @@ class EntityReferenceFromCqlClause extends EntityReference, ExprNode {
748753
}
749754

750755
/**
751-
* The `"data"` property of the handler's parameter that represents the request or message passed to this handler.
752-
* This property carries the user-provided payload provided to the CAP application. e.g.
756+
* The `"data"` property of the handler's parameter that represents the request or message
757+
* passed to this handler. This property carries the user-provided payload provided to the
758+
* CAP application. e.g.
753759
* ``` javascript
754760
* srv.on("send", async (msg) => {
755761
* const { payload } = msg.data;
756762
* })
757763
* ```
758-
* The `payload` carries the data that is sent to this application on the action or event named `send`.
764+
* The `payload` carries the data that is sent to this application on the action or event
765+
* named `send`.
759766
*/
760767
class HandlerParameterData instanceof PropRead {
761768
HandlerParameter handlerParameter;
@@ -807,7 +814,7 @@ abstract class CqlQueryRunnerCall extends MethodCallNode {
807814
string methodName;
808815

809816
CqlQueryRunnerCall() {
810-
this = base.getAMemberCall(methodName) and
817+
this = base.getAMemberInvocation(methodName) and
811818
(
812819
/*
813820
* 1. Method call on the CDS facade or the base database service,
@@ -820,28 +827,87 @@ abstract class CqlQueryRunnerCall extends MethodCallNode {
820827
exists(ServiceInstance srv | base = srv)
821828
)
822829
}
830+
831+
/**
832+
* Gets an argument to this runner call, including the subsequent builder functions
833+
* called in a chained manner on this one.
834+
*/
835+
abstract DataFlow::Node getAQueryParameter();
823836
}
824837

825838
class CqlRunMethodCall extends CqlQueryRunnerCall {
826839
CqlRunMethodCall() { this.getMethodName() = "run" }
840+
841+
override DataFlow::Node getAQueryParameter() { result = this.getArgument(0) }
827842
}
828843

829-
class CqlReadMethodCall extends CqlQueryRunnerCall {
844+
class CqlShortcutMethodCall extends CqlQueryRunnerCall {
845+
CqlShortcutMethodCall() {
846+
this.getMethodName() = ["read", "create", "update", "delete", "insert", "upsert"]
847+
}
848+
849+
abstract override DataFlow::Node getAQueryParameter();
850+
}
851+
852+
class CqlReadMethodCall extends CqlShortcutMethodCall {
830853
CqlReadMethodCall() { this.getMethodName() = "read" }
854+
855+
override DataFlow::Node getAQueryParameter() {
856+
result = this.getAChainedMethodCall(_).getAnArgument()
857+
}
831858
}
832859

833-
class CqlCreateMethodCall extends CqlQueryRunnerCall {
860+
class CqlCreateMethodCall extends CqlShortcutMethodCall {
834861
CqlCreateMethodCall() { this.getMethodName() = "create" }
862+
863+
override DataFlow::Node getAQueryParameter() {
864+
exists(DataFlow::CallNode chainedMethodCall |
865+
chainedMethodCall = this.getAChainedMethodCall(_)
866+
|
867+
result = chainedMethodCall.getAnArgument() or
868+
result = chainedMethodCall.getAnArgument().(SourceNode).getAPropertyWrite().getRhs()
869+
)
870+
}
835871
}
836872

837-
class CqlUpdateMethodCall extends CqlQueryRunnerCall {
873+
class CqlUpdateMethodCall extends CqlShortcutMethodCall {
838874
CqlUpdateMethodCall() { this.getMethodName() = "update" }
875+
876+
override DataFlow::Node getAQueryParameter() {
877+
result = this.getAChainedMethodCall(_).getAnArgument()
878+
}
839879
}
840880

841-
class CqlDeleteMethodCall extends CqlQueryRunnerCall {
881+
class CqlDeleteMethodCall extends CqlShortcutMethodCall {
842882
CqlDeleteMethodCall() { this.getMethodName() = "delete" }
883+
884+
override DataFlow::Node getAQueryParameter() {
885+
result = this.getAChainedMethodCall(_).getAnArgument()
886+
}
843887
}
844888

845-
class CqlInsertMethodCall extends CqlQueryRunnerCall {
889+
class CqlInsertMethodCall extends CqlShortcutMethodCall {
846890
CqlInsertMethodCall() { this.getMethodName() = "insert" }
891+
892+
override DataFlow::Node getAQueryParameter() {
893+
exists(DataFlow::CallNode chainedMethodCall |
894+
chainedMethodCall = this.getAChainedMethodCall(_)
895+
|
896+
result = chainedMethodCall.getAnArgument() or
897+
result = chainedMethodCall.getAnArgument().(SourceNode).getAPropertyWrite().getRhs()
898+
)
899+
}
900+
}
901+
902+
class CqlUpsertMethodCall extends CqlShortcutMethodCall {
903+
CqlUpsertMethodCall() { this.getMethodName() = "upsert" }
904+
905+
override DataFlow::Node getAQueryParameter() {
906+
exists(DataFlow::CallNode chainedMethodCall |
907+
chainedMethodCall = this.getAChainedMethodCall(_)
908+
|
909+
result = chainedMethodCall.getAnArgument() or
910+
result = chainedMethodCall.getAnArgument().(SourceNode).getAPropertyWrite().getRhs()
911+
)
912+
}
847913
}

javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CQL.qll

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,38 @@ class CqlDeleteClause extends CqlClause {
354354
result.asDotExpr().getPropertyName() = "from"
355355
}
356356
}
357+
358+
/**
359+
* A call to APIs that takes the given input string written in CDL and parses it according to
360+
* the CQN specification.
361+
*
362+
* Note that the outcome of calling the fluent APIs is also a CQN, which means both can be run
363+
* against a service with `srv.run`.
364+
*/
365+
abstract class CqlClauseParserCall extends DataFlow::CallNode {
366+
DataFlow::ExprNode cdlString;
367+
368+
DataFlow::ExprNode getCdlString() { result = cdlString }
369+
}
370+
371+
class GlobalCQLFunction extends CqlClauseParserCall {
372+
GlobalCQLFunction() { this = DataFlow::globalVarRef("CQL").getACall() }
373+
}
374+
375+
class CdsParseCqlCall extends CqlClauseParserCall {
376+
CdsParseCqlCall() {
377+
exists(CdsFacade cds |
378+
this = cds.getMember("parse").getMember("cql").getACall() and
379+
cdlString = this.getArgument(0)
380+
)
381+
}
382+
}
383+
384+
class CdsQlCall extends CqlClauseParserCall {
385+
CdsQlCall() {
386+
exists(CdsFacade cds |
387+
this = cds.getMember("ql").getACall() and
388+
cdlString = this.getArgument(0)
389+
)
390+
}
391+
}

javascript/frameworks/cap/src/cqlinjection/CqlInjection.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import javascript
1414
import DataFlow::PathGraph
1515
import advanced_security.javascript.frameworks.cap.CAPCqlInjectionQuery
1616

17-
from CqlIConfiguration sql, DataFlow::PathNode source, DataFlow::PathNode sink
17+
from CqlInjectionConfiguration sql, DataFlow::PathNode source, DataFlow::PathNode sink
1818
where sql.hasFlowPath(source, sink)
1919
select sink.getNode(), source, sink, "This query depends on a $@.", source.getNode(),
2020
"user-provided value"

0 commit comments

Comments
 (0)