Merge branch 'main' into precompute-states-in-overrun-write

This commit is contained in:
Mathias Vorreiter Pedersen
2023-05-16 18:09:16 +01:00
136 changed files with 2090 additions and 680 deletions

View File

@@ -254,6 +254,10 @@ module StringSizeConfig implements ProductFlow::StateConfigSig {
predicate isBarrier2(DataFlow::Node node, FlowState2 state) { none() } predicate isBarrier2(DataFlow::Node node, FlowState2 state) { none() }
predicate isBarrierOut2(DataFlow::Node node) {
node = any(DataFlow::SsaPhiNode phi).getAnInput(true)
}
predicate isAdditionalFlowStep1( predicate isAdditionalFlowStep1(
DataFlow::Node node1, FlowState1 state1, DataFlow::Node node2, FlowState1 state2 DataFlow::Node node1, FlowState1 state1, DataFlow::Node node2, FlowState1 state2
) { ) {

View File

@@ -83,6 +83,7 @@ edges
| test.cpp:243:12:243:14 | str indirection [string] | test.cpp:243:16:243:21 | string indirection | | test.cpp:243:12:243:14 | str indirection [string] | test.cpp:243:16:243:21 | string indirection |
| test.cpp:243:16:243:21 | string indirection | test.cpp:243:12:243:21 | string | | test.cpp:243:16:243:21 | string indirection | test.cpp:243:12:243:21 | string |
| test.cpp:249:20:249:27 | call to my_alloc | test.cpp:250:12:250:12 | p | | test.cpp:249:20:249:27 | call to my_alloc | test.cpp:250:12:250:12 | p |
| test.cpp:256:17:256:22 | call to malloc | test.cpp:257:12:257:12 | p |
nodes nodes
| test.cpp:16:11:16:21 | mk_string_t indirection [string] | semmle.label | mk_string_t indirection [string] | | test.cpp:16:11:16:21 | mk_string_t indirection [string] | semmle.label | mk_string_t indirection [string] |
| test.cpp:18:5:18:30 | ... = ... | semmle.label | ... = ... | | test.cpp:18:5:18:30 | ... = ... | semmle.label | ... = ... |
@@ -159,6 +160,8 @@ nodes
| test.cpp:243:16:243:21 | string indirection | semmle.label | string indirection | | test.cpp:243:16:243:21 | string indirection | semmle.label | string indirection |
| test.cpp:249:20:249:27 | call to my_alloc | semmle.label | call to my_alloc | | test.cpp:249:20:249:27 | call to my_alloc | semmle.label | call to my_alloc |
| test.cpp:250:12:250:12 | p | semmle.label | p | | test.cpp:250:12:250:12 | p | semmle.label | p |
| test.cpp:256:17:256:22 | call to malloc | semmle.label | call to malloc |
| test.cpp:257:12:257:12 | p | semmle.label | p |
subpaths subpaths
| test.cpp:242:22:242:27 | buffer | test.cpp:235:40:235:45 | buffer | test.cpp:236:12:236:17 | p_str indirection [post update] [string] | test.cpp:242:16:242:19 | set_string output argument [string] | | test.cpp:242:22:242:27 | buffer | test.cpp:235:40:235:45 | buffer | test.cpp:236:12:236:17 | p_str indirection [post update] [string] | test.cpp:242:16:242:19 | set_string output argument [string] |
#select #select
@@ -177,6 +180,5 @@ subpaths
| test.cpp:199:9:199:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:199:22:199:27 | string | This write may overflow $@ by 2 elements. | test.cpp:199:22:199:27 | string | string | | test.cpp:199:9:199:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:199:22:199:27 | string | This write may overflow $@ by 2 elements. | test.cpp:199:22:199:27 | string | string |
| test.cpp:203:9:203:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:203:22:203:27 | string | This write may overflow $@ by 2 elements. | test.cpp:203:22:203:27 | string | string | | test.cpp:203:9:203:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:203:22:203:27 | string | This write may overflow $@ by 2 elements. | test.cpp:203:22:203:27 | string | string |
| test.cpp:207:9:207:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:207:22:207:27 | string | This write may overflow $@ by 3 elements. | test.cpp:207:22:207:27 | string | string | | test.cpp:207:9:207:15 | call to strncpy | test.cpp:147:19:147:24 | call to malloc | test.cpp:207:22:207:27 | string | This write may overflow $@ by 3 elements. | test.cpp:207:22:207:27 | string | string |
| test.cpp:232:3:232:8 | call to memset | test.cpp:228:43:228:48 | call to malloc | test.cpp:232:10:232:15 | buffer | This write may overflow $@ by 2 elements. | test.cpp:232:10:232:15 | buffer | buffer |
| test.cpp:243:5:243:10 | call to memset | test.cpp:241:27:241:32 | call to malloc | test.cpp:243:12:243:21 | string | This write may overflow $@ by 1 element. | test.cpp:243:16:243:21 | string | string | | test.cpp:243:5:243:10 | call to memset | test.cpp:241:27:241:32 | call to malloc | test.cpp:243:12:243:21 | string | This write may overflow $@ by 1 element. | test.cpp:243:16:243:21 | string | string |
| test.cpp:250:5:250:10 | call to memset | test.cpp:249:20:249:27 | call to my_alloc | test.cpp:250:12:250:12 | p | This write may overflow $@ by 1 element. | test.cpp:250:12:250:12 | p | p | | test.cpp:250:5:250:10 | call to memset | test.cpp:249:20:249:27 | call to my_alloc | test.cpp:250:12:250:12 | p | This write may overflow $@ by 1 element. | test.cpp:250:12:250:12 | p | p |

View File

@@ -229,7 +229,7 @@ void repeated_alerts(unsigned size, unsigned offset) {
while(unknown()) { while(unknown()) {
++size; ++size;
} }
memset(buffer, 0, size); // BAD memset(buffer, 0, size); // BAD [NOT DETECTED]
} }
void set_string(string_t* p_str, char* buffer) { void set_string(string_t* p_str, char* buffer) {
@@ -249,3 +249,11 @@ void foo(unsigned size) {
int* p = (int*)my_alloc(size); // BAD int* p = (int*)my_alloc(size); // BAD
memset(p, 0, size + 1); memset(p, 0, size + 1);
} }
void test6(unsigned long n, char *p) {
while (unknown()) {
n++;
p = (char *)malloc(n);
memset(p, 0, n); // GOOD
}
}

View File

@@ -649,6 +649,10 @@ edges
| test.cpp:280:13:280:24 | new[] | test.cpp:281:14:281:15 | xs | | test.cpp:280:13:280:24 | new[] | test.cpp:281:14:281:15 | xs |
| test.cpp:290:13:290:24 | new[] | test.cpp:291:14:291:15 | xs | | test.cpp:290:13:290:24 | new[] | test.cpp:291:14:291:15 | xs |
| test.cpp:290:13:290:24 | new[] | test.cpp:292:30:292:30 | x | | test.cpp:290:13:290:24 | new[] | test.cpp:292:30:292:30 | x |
| test.cpp:304:15:304:26 | new[] | test.cpp:307:5:307:6 | xs |
| test.cpp:304:15:304:26 | new[] | test.cpp:308:5:308:6 | xs |
| test.cpp:308:5:308:6 | xs | test.cpp:308:5:308:11 | access to array |
| test.cpp:308:5:308:11 | access to array | test.cpp:308:5:308:29 | Store: ... = ... |
#select #select
| test.cpp:6:14:6:15 | Load: * ... | test.cpp:4:15:4:20 | call to malloc | test.cpp:6:14:6:15 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:4:15:4:20 | call to malloc | call to malloc | test.cpp:5:19:5:22 | size | size | | test.cpp:6:14:6:15 | Load: * ... | test.cpp:4:15:4:20 | call to malloc | test.cpp:6:14:6:15 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:4:15:4:20 | call to malloc | call to malloc | test.cpp:5:19:5:22 | size | size |
| test.cpp:8:14:8:21 | Load: * ... | test.cpp:4:15:4:20 | call to malloc | test.cpp:8:14:8:21 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@ + 1. | test.cpp:4:15:4:20 | call to malloc | call to malloc | test.cpp:5:19:5:22 | size | size | | test.cpp:8:14:8:21 | Load: * ... | test.cpp:4:15:4:20 | call to malloc | test.cpp:8:14:8:21 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@ + 1. | test.cpp:4:15:4:20 | call to malloc | call to malloc | test.cpp:5:19:5:22 | size | size |
@@ -672,3 +676,4 @@ edges
| test.cpp:254:9:254:16 | Store: ... = ... | test.cpp:248:24:248:30 | call to realloc | test.cpp:254:9:254:16 | Store: ... = ... | This write might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:248:24:248:30 | call to realloc | call to realloc | test.cpp:254:11:254:11 | i | i | | test.cpp:254:9:254:16 | Store: ... = ... | test.cpp:248:24:248:30 | call to realloc | test.cpp:254:9:254:16 | Store: ... = ... | This write might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:248:24:248:30 | call to realloc | call to realloc | test.cpp:254:11:254:11 | i | i |
| test.cpp:264:13:264:14 | Load: * ... | test.cpp:260:13:260:24 | new[] | test.cpp:264:13:264:14 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:260:13:260:24 | new[] | new[] | test.cpp:261:19:261:21 | len | len | | test.cpp:264:13:264:14 | Load: * ... | test.cpp:260:13:260:24 | new[] | test.cpp:264:13:264:14 | Load: * ... | This read might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:260:13:260:24 | new[] | new[] | test.cpp:261:19:261:21 | len | len |
| test.cpp:274:5:274:10 | Store: ... = ... | test.cpp:270:13:270:24 | new[] | test.cpp:274:5:274:10 | Store: ... = ... | This write might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:270:13:270:24 | new[] | new[] | test.cpp:271:19:271:21 | len | len | | test.cpp:274:5:274:10 | Store: ... = ... | test.cpp:270:13:270:24 | new[] | test.cpp:274:5:274:10 | Store: ... = ... | This write might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:270:13:270:24 | new[] | new[] | test.cpp:271:19:271:21 | len | len |
| test.cpp:308:5:308:29 | Store: ... = ... | test.cpp:304:15:304:26 | new[] | test.cpp:308:5:308:29 | Store: ... = ... | This write might be out of bounds, as the pointer might be equal to $@ + $@. | test.cpp:304:15:304:26 | new[] | new[] | test.cpp:308:8:308:10 | ... + ... | ... + ... |

View File

@@ -294,3 +294,17 @@ void test20(unsigned len)
*x = 0; // GOOD *x = 0; // GOOD
} }
} }
void* test21_get(int n);
void test21() {
int n = 0;
while (test21_get(n)) n+=2;
void** xs = new void*[n];
for (int i = 0; i < n; i += 2) {
xs[i] = test21_get(i);
xs[i+1] = test21_get(i+1);
}
}

View File

@@ -125,6 +125,8 @@ postWithInFlow
| test.cpp:681:3:681:3 | s [post update] | PostUpdateNode should not be the target of local flow. | | test.cpp:681:3:681:3 | s [post update] | PostUpdateNode should not be the target of local flow. |
| test.cpp:689:3:689:3 | s [post update] | PostUpdateNode should not be the target of local flow. | | test.cpp:689:3:689:3 | s [post update] | PostUpdateNode should not be the target of local flow. |
| test.cpp:690:3:690:3 | s [post update] | PostUpdateNode should not be the target of local flow. | | test.cpp:690:3:690:3 | s [post update] | PostUpdateNode should not be the target of local flow. |
| test.cpp:694:4:694:6 | buf [inner post update] | PostUpdateNode should not be the target of local flow. |
| test.cpp:704:23:704:25 | buf [inner post update] | PostUpdateNode should not be the target of local flow. |
viableImplInCallContextTooLarge viableImplInCallContextTooLarge
uniqueParameterNodeAtPosition uniqueParameterNodeAtPosition
uniqueParameterNodePosition uniqueParameterNodePosition

View File

@@ -690,3 +690,16 @@ void test_static_local_9() {
s = 0; s = 0;
} }
void increment_buf(int** buf) { // $ ast-def=buf ir-def=*buf ir-def=**buf
*buf += 10;
sink(buf); // $ SPURIOUS: ast,ir // should only be flow to the indirect argument, but there's also flow to the non-indirect argument
}
void call_increment_buf(int** buf) { // $ ast-def=buf
increment_buf(buf);
}
void test_conflation_regression(int* source) { // $ ast-def=source
int* buf = source;
call_increment_buf(&buf);
}

View File

@@ -49,3 +49,13 @@
return 0; return 0;
} }
void* f3_get(int n);
void f3() {
int n = 0;
while (f3_get(n)) n+=2;
for (int i = 0; i < n; i += 2) {
range(i); // $ range=>=0 SPURIOUS: range="<=call to f3_get-1" range="<=call to f3_get-2"
}
}

View File

@@ -72,5 +72,5 @@ private class MyConsistencyConfiguration extends ConsistencyConfiguration {
override predicate reverseReadExclude(Node n) { n.asExpr() = any(AwaitExpr ae).getExpr() } override predicate reverseReadExclude(Node n) { n.asExpr() = any(AwaitExpr ae).getExpr() }
override predicate identityLocalStepExclude(Node n) { n.getLocation().getFile().fromLibrary() } override predicate identityLocalStepExclude(Node n) { none() }
} }

View File

@@ -335,7 +335,8 @@ module LocalFlow {
exists(ControlFlow::BasicBlock bb, int i | exists(ControlFlow::BasicBlock bb, int i |
SsaImpl::lastRefBeforeRedefExt(def, bb, i, next.getDefinitionExt()) and SsaImpl::lastRefBeforeRedefExt(def, bb, i, next.getDefinitionExt()) and
def.definesAt(_, bb, i, _) and def.definesAt(_, bb, i, _) and
def = getSsaDefinitionExt(nodeFrom) def = getSsaDefinitionExt(nodeFrom) and
nodeFrom != next
) )
} }
@@ -414,7 +415,8 @@ module LocalFlow {
) { ) {
exists(CIL::BasicBlock bb, int i | CilSsaImpl::lastRefBeforeRedefExt(def, bb, i, next) | exists(CIL::BasicBlock bb, int i | CilSsaImpl::lastRefBeforeRedefExt(def, bb, i, next) |
def.definesAt(_, bb, i, _) and def.definesAt(_, bb, i, _) and
def = nodeFrom.(CilSsaDefinitionExtNode).getDefinition() def = nodeFrom.(CilSsaDefinitionExtNode).getDefinition() and
def != next
or or
nodeFrom = TCilExprNode(bb.getNode(i).(CIL::ReadAccess)) nodeFrom = TCilExprNode(bb.getNode(i).(CIL::ReadAccess))
) )
@@ -440,7 +442,8 @@ module LocalFlow {
exists(CIL::ReadAccess readFrom, CIL::ReadAccess readTo | exists(CIL::ReadAccess readFrom, CIL::ReadAccess readTo |
CilSsaImpl::hasAdjacentReadsExt(def, readFrom, readTo) and CilSsaImpl::hasAdjacentReadsExt(def, readFrom, readTo) and
nodeTo = TCilExprNode(readTo) and nodeTo = TCilExprNode(readTo) and
nodeFrom = TCilExprNode(readFrom) nodeFrom = TCilExprNode(readFrom) and
nodeFrom != nodeTo
) )
or or
// Flow into phi (read) node // Flow into phi (read) node
@@ -483,7 +486,8 @@ module LocalFlow {
or or
hasNodePath(any(LocalExprStepConfiguration x), nodeFrom, nodeTo) hasNodePath(any(LocalExprStepConfiguration x), nodeFrom, nodeTo)
or or
ThisFlow::adjacentThisRefs(nodeFrom, nodeTo) ThisFlow::adjacentThisRefs(nodeFrom, nodeTo) and
nodeFrom != nodeTo
or or
ThisFlow::adjacentThisRefs(nodeFrom.(PostUpdateNode).getPreUpdateNode(), nodeTo) ThisFlow::adjacentThisRefs(nodeFrom.(PostUpdateNode).getPreUpdateNode(), nodeTo)
or or
@@ -541,7 +545,8 @@ predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) {
exists(SsaImpl::DefinitionExt def | exists(SsaImpl::DefinitionExt def |
LocalFlow::localSsaFlowStepUseUse(def, nodeFrom, nodeTo) and LocalFlow::localSsaFlowStepUseUse(def, nodeFrom, nodeTo) and
not FlowSummaryImpl::Private::Steps::prohibitsUseUseFlow(nodeFrom, _) and not FlowSummaryImpl::Private::Steps::prohibitsUseUseFlow(nodeFrom, _) and
not LocalFlow::usesInstanceField(def) not LocalFlow::usesInstanceField(def) and
nodeFrom != nodeTo
) )
or or
// Flow into phi (read)/uncertain SSA definition node from read // Flow into phi (read)/uncertain SSA definition node from read
@@ -880,7 +885,8 @@ private module Cached {
predicate localFlowStepImpl(Node nodeFrom, Node nodeTo) { predicate localFlowStepImpl(Node nodeFrom, Node nodeTo) {
LocalFlow::localFlowStepCommon(nodeFrom, nodeTo) LocalFlow::localFlowStepCommon(nodeFrom, nodeTo)
or or
LocalFlow::localSsaFlowStepUseUse(_, nodeFrom, nodeTo) LocalFlow::localSsaFlowStepUseUse(_, nodeFrom, nodeTo) and
nodeFrom != nodeTo
or or
exists(SsaImpl::DefinitionExt def | exists(SsaImpl::DefinitionExt def |
LocalFlow::localSsaFlowStep(def, nodeFrom, nodeTo) and LocalFlow::localSsaFlowStep(def, nodeFrom, nodeTo) and

View File

@@ -1,7 +0,0 @@
identityLocalStep
| test.cs:17:41:17:44 | this access | Node steps to itself |
| test.cs:34:41:34:44 | this access | Node steps to itself |
| test.cs:52:41:52:44 | this access | Node steps to itself |
| test.cs:67:41:67:44 | this access | Node steps to itself |
| test.cs:77:22:77:24 | this access | Node steps to itself |
| test.cs:90:41:90:44 | this access | Node steps to itself |

View File

@@ -1,2 +0,0 @@
identityLocalStep
| Conditions.cs:133:17:133:22 | [Field1 (line 129): false] this access | Node steps to itself |

View File

@@ -1,2 +0,0 @@
identityLocalStep
| Splitting.cs:133:21:133:29 | [b (line 123): false] this access | Node steps to itself |

View File

@@ -1,7 +0,0 @@
identityLocalStep
| SplittingStressTest.cs:172:16:172:16 | SSA phi read(b29) | Node steps to itself |
| SplittingStressTest.cs:179:13:183:13 | [b1 (line 170): false] SSA phi read(b1) | Node steps to itself |
| SplittingStressTest.cs:184:13:188:13 | [b2 (line 170): false] SSA phi read(b2) | Node steps to itself |
| SplittingStressTest.cs:189:13:193:13 | [b3 (line 170): false] SSA phi read(b3) | Node steps to itself |
| SplittingStressTest.cs:194:13:198:13 | [b4 (line 170): false] SSA phi read(b4) | Node steps to itself |
| SplittingStressTest.cs:199:13:203:13 | [b5 (line 170): false] SSA phi read(b5) | Node steps to itself |

View File

@@ -1,2 +0,0 @@
identityLocalStep
| Test.cs:80:37:80:42 | this access | Node steps to itself |

View File

@@ -1,2 +0,0 @@
identityLocalStep
| GlobalDataFlow.cs:573:9:576:9 | SSA phi read(f) | Node steps to itself |

View File

@@ -1,3 +0,0 @@
identityLocalStep
| DefUse.cs:80:37:80:42 | this access | Node steps to itself |
| Properties.cs:65:24:65:31 | this access | Node steps to itself |

View File

@@ -1,4 +0,0 @@
identityLocalStep
| D.cs:320:17:320:25 | this access | Node steps to itself |
| E.cs:123:21:123:24 | SSA phi read(x) | Node steps to itself |
| E.cs:123:21:123:24 | SSA phi(i) | Node steps to itself |

View File

@@ -1,2 +0,0 @@
identityLocalStep
| ZipSlip.cs:13:13:45:13 | SSA phi read(destDirectory) | Node steps to itself |

View File

@@ -176,7 +176,11 @@ A QL module definition has the following syntax:
:: ::
module ::= annotation* "module" modulename "{" moduleBody "}" module ::= annotation* "module" modulename parameters? implements? "{" moduleBody "}"
parameters ::= "<" signatureExpr simpleId ("," signatureExpr simpleId)* ">"
implements ::= "implements" moduleSignatureExpr ("," moduleSignatureExpr)*
moduleBody ::= (import | predicate | class | module | alias | select)* moduleBody ::= (import | predicate | class | module | alias | select)*
@@ -208,12 +212,15 @@ An import directive refers to a module identifier:
:: ::
import ::= annotations "import" importModuleId ("as" modulename)? import ::= annotations "import" importModuleExpr ("as" modulename)?
qualId ::= simpleId | qualId "." simpleId qualId ::= simpleId | qualId "." simpleId
importModuleId ::= qualId importModuleExpr ::= qualId | importModuleExpr "::" simpleId arguments?
| importModuleId "::" simpleId
arguments ::= "<" argument ("," argument)* ">"
argument ::= moduleExpr | type | predicateRef "/" int
An import directive may optionally name the imported module using an ``as`` declaration. If a name is defined, then the import directive adds to the declared module environment of the current module a mapping from the name to the declaration of the imported module. Otherwise, the current module *directly imports* the imported module. An import directive may optionally name the imported module using an ``as`` declaration. If a name is defined, then the import directive adds to the declared module environment of the current module a mapping from the name to the declaration of the imported module. Otherwise, the current module *directly imports* the imported module.
@@ -280,9 +287,9 @@ With the exception of class domain types and character types (which cannot be re
:: ::
type ::= (moduleId "::")? classname | dbasetype | "boolean" | "date" | "float" | "int" | "string" type ::= (moduleExpr "::")? classname | dbasetype | "boolean" | "date" | "float" | "int" | "string"
moduleId ::= simpleId | moduleId "::" simpleId moduleExpr ::= simpleId arguments? | moduleExpr "::" simpleId arguments?
A type reference is resolved to a type as follows: A type reference is resolved to a type as follows:
@@ -597,7 +604,7 @@ Identifiers are used in following syntactic constructs:
modulename ::= simpleId modulename ::= simpleId
classname ::= upperId classname ::= upperId
dbasetype ::= atLowerId dbasetype ::= atLowerId
predicateRef ::= (moduleId "::")? literalId predicateRef ::= (moduleExpr "::")? literalId
predicateName ::= lowerId predicateName ::= lowerId
varname ::= lowerId varname ::= lowerId
literalId ::= lowerId | atLowerId literalId ::= lowerId | atLowerId
@@ -1615,7 +1622,7 @@ Aliases define new names for existing QL entities.
alias ::= qldoc? annotations "predicate" literalId "=" predicateRef "/" int ";" alias ::= qldoc? annotations "predicate" literalId "=" predicateRef "/" int ";"
| qldoc? annotations "class" classname "=" type ";" | qldoc? annotations "class" classname "=" type ";"
| qldoc? annotations "module" modulename "=" moduleId ";" | qldoc? annotations "module" modulename "=" moduleExpr ";"
An alias introduces a binding from the new name to the entity referred to by the right-hand side in the current module's declared predicate, type, or module environment respectively. An alias introduces a binding from the new name to the entity referred to by the right-hand side in the current module's declared predicate, type, or module environment respectively.
@@ -2064,16 +2071,23 @@ The complete grammar for QL is as follows:
ql ::= qldoc? moduleBody ql ::= qldoc? moduleBody
module ::= annotation* "module" modulename "{" moduleBody "}" module ::= annotation* "module" modulename parameters? implements? "{" moduleBody "}"
parameters ::= "<" signatureExpr simpleId ("," signatureExpr simpleId)* ">"
implements ::= "implements" moduleSignatureExpr ("," moduleSignatureExpr)*
moduleBody ::= (import | predicate | class | module | alias | select)* moduleBody ::= (import | predicate | class | module | alias | select)*
import ::= annotations "import" importModuleId ("as" modulename)? import ::= annotations "import" importModuleExpr ("as" modulename)?
qualId ::= simpleId | qualId "." simpleId qualId ::= simpleId | qualId "." simpleId
importModuleId ::= qualId importModuleExpr ::= qualId | importModuleExpr "::" simpleId arguments?
| importModuleId "::" simpleId
arguments ::= "<" argument ("," argument)* ">"
argument ::= moduleExpr | type | predicateRef "/" int
select ::= ("from" var_decls)? ("where" formula)? "select" as_exprs ("order" "by" orderbys)? select ::= ("from" var_decls)? ("where" formula)? "select" as_exprs ("order" "by" orderbys)?
@@ -2120,15 +2134,19 @@ The complete grammar for QL is as follows:
field ::= qldoc? annotations var_decl ";" field ::= qldoc? annotations var_decl ";"
moduleId ::= simpleId | moduleId "::" simpleId moduleExpr ::= simpleId arguments? | moduleExpr "::" simpleId arguments?
type ::= (moduleId "::")? classname | dbasetype | "boolean" | "date" | "float" | "int" | "string" moduleSignatureExpr ::= (moduleExpr "::")? upperId arguments?
signatureExpr : (moduleExpr "::")? simpleId ("/" Integer | arguments)?;
type ::= (moduleExpr "::")? classname | dbasetype | "boolean" | "date" | "float" | "int" | "string"
exprs ::= expr ("," expr)* exprs ::= expr ("," expr)*
alias ::= qldoc? annotations "predicate" literalId "=" predicateRef "/" int ";" alias ::= qldoc? annotations "predicate" literalId "=" predicateRef "/" int ";"
| qldoc? annotations "class" classname "=" type ";" | qldoc? annotations "class" classname "=" type ";"
| qldoc? annotations "module" modulename "=" moduleId ";" | qldoc? annotations "module" modulename "=" moduleExpr ";"
var_decls ::= (var_decl ("," var_decl)*)? var_decls ::= (var_decl ("," var_decl)*)?
@@ -2249,7 +2267,7 @@ The complete grammar for QL is as follows:
dbasetype ::= atLowerId dbasetype ::= atLowerId
predicateRef ::= (moduleId "::")? literalId predicateRef ::= (moduleExpr "::")? literalId
predicateName ::= lowerId predicateName ::= lowerId

View File

@@ -115,7 +115,7 @@ test: all build/testdb/check-upgrade-path
codeql test run -j0 ql/test --search-path build/codeql-extractor-go --consistency-queries ql/test/consistency --compilation-cache=$(cache) codeql test run -j0 ql/test --search-path build/codeql-extractor-go --consistency-queries ql/test/consistency --compilation-cache=$(cache)
# use GOOS=linux because GOOS=darwin GOARCH=386 is no longer supported # use GOOS=linux because GOOS=darwin GOARCH=386 is no longer supported
env GOOS=linux GOARCH=386 codeql$(EXE) test run -j0 ql/test/query-tests/Security/CWE-681 --search-path build/codeql-extractor-go --consistency-queries ql/test/consistency --compilation-cache=$(cache) env GOOS=linux GOARCH=386 codeql$(EXE) test run -j0 ql/test/query-tests/Security/CWE-681 --search-path build/codeql-extractor-go --consistency-queries ql/test/consistency --compilation-cache=$(cache)
cd extractor; go test -mod=vendor ./... | grep -vF "[no test files]" cd extractor; go test -mod=vendor ./...
bash extractor-smoke-test/test.sh || (echo "Extractor smoke test FAILED"; exit 1) bash extractor-smoke-test/test.sh || (echo "Extractor smoke test FAILED"; exit 1)
.PHONY: build/testdb/check-upgrade-path .PHONY: build/testdb/check-upgrade-path

View File

@@ -36,45 +36,37 @@ func TestParseGoVersion(t *testing.T) {
func TestGetVersionToInstall(t *testing.T) { func TestGetVersionToInstall(t *testing.T) {
tests := map[versionInfo]string{ tests := map[versionInfo]string{
// checkForUnsupportedVersions() // getVersionWhenGoModVersionNotFound()
// go.mod version below minGoVersion
{"0.0", true, "1.20.3", true}: "",
{"0.0", true, "9999.0", true}: "",
{"0.0", true, "1.2.2", true}: "",
{"0.0", true, "", false}: "",
// go.mod version above maxGoVersion
{"9999.0", true, "1.20.3", true}: "",
{"9999.0", true, "9999.0.1", true}: "",
{"9999.0", true, "1.1", true}: "",
{"9999.0", true, "", false}: "",
// Go installation found with version below minGoVersion
{"1.20", true, "1.2.2", true}: "1.20",
{"1.11", true, "1.2.2", true}: "1.11",
{"", false, "1.2.2", true}: maxGoVersion,
// Go installation found with version above maxGoVersion
{"1.20", true, "9999.0.1", true}: "1.20",
{"1.11", true, "9999.0.1", true}: "1.11",
{"", false, "9999.0.1", true}: maxGoVersion,
// checkForVersionsNotFound()
// Go installation not found, go.mod version in supported range
{"1.20", true, "", false}: "1.20",
{"1.11", true, "", false}: "1.11",
// Go installation not found, go.mod not found
{"", false, "", false}: maxGoVersion, {"", false, "", false}: maxGoVersion,
// Go installation found with version in supported range, go.mod not found {"", false, "1.2.2", true}: maxGoVersion,
{"", false, "9999.0.1", true}: maxGoVersion,
{"", false, "1.11.13", true}: "", {"", false, "1.11.13", true}: "",
{"", false, "1.20.3", true}: "", {"", false, "1.20.3", true}: "",
// compareVersions() // getVersionWhenGoModVersionTooHigh()
{"9999.0", true, "", false}: maxGoVersion,
{"9999.0", true, "9999.0.1", true}: "",
{"9999.0", true, "1.1", true}: maxGoVersion,
{"9999.0", true, minGoVersion, false}: maxGoVersion,
{"9999.0", true, maxGoVersion, true}: "",
// Go installation found with version in supported range, go.mod version in supported range and go.mod version > go installation version // getVersionWhenGoModVersionTooLow()
{"0.0", true, "", false}: minGoVersion,
{"0.0", true, "9999.0", true}: minGoVersion,
{"0.0", true, "1.2.2", true}: minGoVersion,
{"0.0", true, "1.20.3", true}: "",
// getVersionWhenGoModVersionSupported()
{"1.20", true, "", false}: "1.20",
{"1.11", true, "", false}: "1.11",
{"1.20", true, "1.2.2", true}: "1.20",
{"1.11", true, "1.2.2", true}: "1.11",
{"1.20", true, "9999.0.1", true}: "1.20",
{"1.11", true, "9999.0.1", true}: "1.11",
// go.mod version > go installation version
{"1.20", true, "1.11.13", true}: "1.20", {"1.20", true, "1.11.13", true}: "1.20",
{"1.20", true, "1.12", true}: "1.20", {"1.20", true, "1.12", true}: "1.20",
// Go installation found with version in supported range, go.mod version in supported range and go.mod version <= go installation version // go.mod version <= go installation version (Note comparisons ignore the patch version)
// (Note comparisons ignore the patch version)
{"1.11", true, "1.20", true}: "", {"1.11", true, "1.20", true}: "",
{"1.11", true, "1.20.3", true}: "", {"1.11", true, "1.20.3", true}: "",
{"1.20", true, "1.20.3", true}: "", {"1.20", true, "1.20.3", true}: "",

View File

@@ -78,7 +78,7 @@ class InlineFlowTest extends InlineExpectationsTest {
override predicate hasActualResult(Location location, string element, string tag, string value) { override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasValueFlow" and tag = "hasValueFlow" and
exists(DataFlow::Node sink | getValueFlowConfig().hasFlowTo(sink) | exists(DataFlow::Node sink | this.getValueFlowConfig().hasFlowTo(sink) |
sink.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(), sink.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(),
location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and
element = sink.toString() and element = sink.toString() and
@@ -87,7 +87,8 @@ class InlineFlowTest extends InlineExpectationsTest {
or or
tag = "hasTaintFlow" and tag = "hasTaintFlow" and
exists(DataFlow::Node src, DataFlow::Node sink | exists(DataFlow::Node src, DataFlow::Node sink |
getTaintFlowConfig().hasFlow(src, sink) and not getValueFlowConfig().hasFlow(src, sink) this.getTaintFlowConfig().hasFlow(src, sink) and
not this.getValueFlowConfig().hasFlow(src, sink)
| |
sink.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(), sink.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(),
location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and

View File

@@ -239,8 +239,6 @@ open class KotlinUsesExtractor(
return UseClassInstanceResult(classTypeResult, extractClass) return UseClassInstanceResult(classTypeResult, extractClass)
} }
private fun isArray(t: IrSimpleType) = t.isBoxedArray || t.isPrimitiveArray()
private fun extractClassLaterIfExternal(c: IrClass) { private fun extractClassLaterIfExternal(c: IrClass) {
if (isExternalDeclaration(c)) { if (isExternalDeclaration(c)) {
extractExternalClassLater(c) extractExternalClassLater(c)
@@ -551,6 +549,22 @@ open class KotlinUsesExtractor(
) )
} }
/*
Kotlin arrays can be broken down as:
isArray(t)
|- t.isBoxedArray
| |- t.isArray() e.g. Array<Boolean>, Array<Boolean?>
| |- t.isNullableArray() e.g. Array<Boolean>?, Array<Boolean?>?
|- t.isPrimitiveArray() e.g. BooleanArray
For the corresponding Java types:
Boxed arrays are represented as e.g. java.lang.Boolean[].
Primitive arrays are represented as e.g. boolean[].
*/
private fun isArray(t: IrType) = t.isBoxedArray || t.isPrimitiveArray()
data class ArrayInfo(val elementTypeResults: TypeResults, data class ArrayInfo(val elementTypeResults: TypeResults,
val componentTypeResults: TypeResults, val componentTypeResults: TypeResults,
val dimensions: Int) val dimensions: Int)
@@ -565,7 +579,7 @@ open class KotlinUsesExtractor(
*/ */
private fun useArrayType(t: IrType, isPrimitiveArray: Boolean): ArrayInfo { private fun useArrayType(t: IrType, isPrimitiveArray: Boolean): ArrayInfo {
if (!t.isBoxedArray && !t.isPrimitiveArray()) { if (!isArray(t)) {
val nullableT = if (t.isPrimitiveType() && !isPrimitiveArray) t.makeNullable() else t val nullableT = if (t.isPrimitiveType() && !isPrimitiveArray) t.makeNullable() else t
val typeResults = useType(nullableT) val typeResults = useType(nullableT)
return ArrayInfo(typeResults, typeResults, 0) return ArrayInfo(typeResults, typeResults, 0)
@@ -1141,13 +1155,13 @@ open class KotlinUsesExtractor(
} }
} else { } else {
t.classOrNull?.let { tCls -> t.classOrNull?.let { tCls ->
if (t.isArray() || t.isNullableArray()) { if (t.isBoxedArray) {
(t.arguments.singleOrNull() as? IrTypeProjection)?.let { elementTypeArg -> (t.arguments.singleOrNull() as? IrTypeProjection)?.let { elementTypeArg ->
val elementType = elementTypeArg.type val elementType = elementTypeArg.type
val replacedElementType = kClassToJavaClass(elementType) val replacedElementType = kClassToJavaClass(elementType)
if (replacedElementType !== elementType) { if (replacedElementType !== elementType) {
val newArg = makeTypeProjection(replacedElementType, elementTypeArg.variance) val newArg = makeTypeProjection(replacedElementType, elementTypeArg.variance)
return tCls.typeWithArguments(listOf(newArg)).codeQlWithHasQuestionMark(t.isNullableArray()) return tCls.typeWithArguments(listOf(newArg)).codeQlWithHasQuestionMark(t.isNullable())
} }
} }
} }
@@ -1578,7 +1592,7 @@ open class KotlinUsesExtractor(
} }
if (owner is IrClass) { if (owner is IrClass) {
if (t.isArray() || t.isNullableArray()) { if (t.isBoxedArray) {
val elementType = t.getArrayElementType(pluginContext.irBuiltIns) val elementType = t.getArrayElementType(pluginContext.irBuiltIns)
val erasedElementType = erase(elementType) val erasedElementType = erase(elementType)
return owner.typeWith(erasedElementType).codeQlWithHasQuestionMark(t.isNullable()) return owner.typeWith(erasedElementType).codeQlWithHasQuestionMark(t.isNullable())

View File

@@ -8,6 +8,11 @@ if "JAVA_HOME_8_X64" in os.environ:
sep = ";" if platform.system() == "Windows" else ":" sep = ";" if platform.system() == "Windows" else ":"
os.environ["PATH"] = "".join([os.path.join(os.environ["JAVA_HOME"], "bin"), sep, os.environ["PATH"]]) os.environ["PATH"] = "".join([os.path.join(os.environ["JAVA_HOME"], "bin"), sep, os.environ["PATH"]])
# Ensure the autobuilder *doesn't* see Java 11 or 17, which it could switch to in order to build the project:
for k in ["JAVA_HOME_11_X64", "JAVA_HOME_17_X64"]:
if k in os.environ:
del os.environ[k]
run_codeql_database_create([], lang="java", runFunction = runUnsuccessfully, db = None) run_codeql_database_create([], lang="java", runFunction = runUnsuccessfully, db = None)
check_diagnostics() check_diagnostics()

View File

@@ -27,8 +27,8 @@ deprecated class CamelToURI = CamelToUri;
class CamelToBeanUri extends CamelToUri { class CamelToBeanUri extends CamelToUri {
CamelToBeanUri() { CamelToBeanUri() {
// A `<to>` element references a bean if the URI starts with "bean:", or there is no scheme. // A `<to>` element references a bean if the URI starts with "bean:", or there is no scheme.
matches("bean:%") or this.matches("bean:%") or
not exists(indexOf(":")) not exists(this.indexOf(":"))
} }
/** /**
@@ -38,13 +38,13 @@ class CamelToBeanUri extends CamelToUri {
* parameter parts are optional. * parameter parts are optional.
*/ */
string getBeanIdentifier() { string getBeanIdentifier() {
if not exists(indexOf(":")) if not exists(this.indexOf(":"))
then result = this then result = this
else else
exists(int start | start = indexOf(":", 0, 0) + 1 | exists(int start | start = this.indexOf(":", 0, 0) + 1 |
if not exists(indexOf("?")) if not exists(this.indexOf("?"))
then result = suffix(start) then result = this.suffix(start)
else result = substring(start, indexOf("?", 0, 0)) else result = this.substring(start, this.indexOf("?", 0, 0))
) )
} }

View File

@@ -655,6 +655,11 @@ class XmlReader extends RefType {
XmlReader() { this.hasQualifiedName("org.xml.sax", "XMLReader") } XmlReader() { this.hasQualifiedName("org.xml.sax", "XMLReader") }
} }
/** The class `org.xml.sax.InputSource`. */
class InputSource extends Class {
InputSource() { this.hasQualifiedName("org.xml.sax", "InputSource") }
}
/** DEPRECATED: Alias for XmlReader */ /** DEPRECATED: Alias for XmlReader */
deprecated class XMLReader = XmlReader; deprecated class XMLReader = XmlReader;
@@ -1164,22 +1169,34 @@ class XmlUnmarshal extends XmlParserCall {
} }
/* XPathExpression: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#xpathexpression */ /* XPathExpression: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#xpathexpression */
/** The class `javax.xml.xpath.XPathExpression`. */ /** The interface `javax.xml.xpath.XPathExpression`. */
class XPathExpression extends RefType { class XPathExpression extends Interface {
XPathExpression() { this.hasQualifiedName("javax.xml.xpath", "XPathExpression") } XPathExpression() { this.hasQualifiedName("javax.xml.xpath", "XPathExpression") }
} }
/** A call to `XPathExpression.evaluate`. */ /** The interface `java.xml.xpath.XPath`. */
class XPath extends Interface {
XPath() { this.hasQualifiedName("javax.xml.xpath", "XPath") }
}
/** A call to the method `evaluate` of the classes `XPathExpression` or `XPath`. */
class XPathEvaluate extends XmlParserCall { class XPathEvaluate extends XmlParserCall {
Argument sink;
XPathEvaluate() { XPathEvaluate() {
exists(Method m | exists(Method m |
this.getMethod() = m and this.getMethod() = m and
m.getDeclaringType() instanceof XPathExpression and
m.hasName("evaluate") m.hasName("evaluate")
|
m.getDeclaringType().getASourceSupertype*() instanceof XPathExpression and
sink = this.getArgument(0)
or
m.getDeclaringType().getASourceSupertype*() instanceof XPath and
sink = this.getArgument(1)
) )
} }
override Expr getSink() { result = this.getArgument(0) } override Expr getSink() { result = sink }
override predicate isSafe() { none() } override predicate isSafe() { none() }
} }

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The queries `java/xxe` and `java/xxe-local` now recognize the second argument of calls to `XPath.evaluate` as a sink.

View File

@@ -12,4 +12,4 @@ dependencies:
codeql/util: ${workspace} codeql/util: ${workspace}
dataExtensions: dataExtensions:
- Telemetry/ExtractorInformation.yml - Telemetry/ExtractorInformation.yml
warnOmImplicitThis: true warnOnImplicitThis: true

View File

@@ -73,7 +73,7 @@ class InlineFlowTest extends InlineExpectationsTest {
override predicate hasActualResult(Location location, string element, string tag, string value) { override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasValueFlow" and tag = "hasValueFlow" and
exists(DataFlow::Node src, DataFlow::Node sink | hasValueFlow(src, sink) | exists(DataFlow::Node src, DataFlow::Node sink | this.hasValueFlow(src, sink) |
sink.getLocation() = location and sink.getLocation() = location and
element = sink.toString() and element = sink.toString() and
if exists(getSourceArgString(src)) then value = getSourceArgString(src) else value = "" if exists(getSourceArgString(src)) then value = getSourceArgString(src) else value = ""
@@ -81,7 +81,7 @@ class InlineFlowTest extends InlineExpectationsTest {
or or
tag = "hasTaintFlow" and tag = "hasTaintFlow" and
exists(DataFlow::Node src, DataFlow::Node sink | exists(DataFlow::Node src, DataFlow::Node sink |
hasTaintFlow(src, sink) and not hasValueFlow(src, sink) this.hasTaintFlow(src, sink) and not this.hasValueFlow(src, sink)
| |
sink.getLocation() = location and sink.getLocation() = location and
element = sink.toString() and element = sink.toString() and

View File

@@ -26,4 +26,19 @@ public class XPathExpressionTests {
XPathExpression expr = path.compile(""); XPathExpression expr = path.compile("");
expr.evaluate(new InputSource(sock.getInputStream())); // unsafe expr.evaluate(new InputSource(sock.getInputStream())); // unsafe
} }
public void safeXPathEvaluateTest(Socket sock) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
XPathFactory xFactory = XPathFactory.newInstance();
XPath path = xFactory.newXPath();
path.evaluate("", builder.parse(sock.getInputStream()));
}
public void unsafeXPathEvaluateTest(Socket sock) throws Exception {
XPathFactory xFactory = XPathFactory.newInstance();
XPath path = xFactory.newXPath();
path.evaluate("", new InputSource(sock.getInputStream())); // unsafe
}
} }

View File

@@ -74,7 +74,8 @@ edges
| XMLReaderTests.java:86:34:86:54 | getInputStream(...) : InputStream | XMLReaderTests.java:86:18:86:55 | new InputSource(...) | | XMLReaderTests.java:86:34:86:54 | getInputStream(...) : InputStream | XMLReaderTests.java:86:18:86:55 | new InputSource(...) |
| XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | XMLReaderTests.java:94:18:94:55 | new InputSource(...) | | XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | XMLReaderTests.java:94:18:94:55 | new InputSource(...) |
| XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | XMLReaderTests.java:100:18:100:55 | new InputSource(...) | | XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | XMLReaderTests.java:100:18:100:55 | new InputSource(...) |
| XPathExpressionTests.java:27:37:27:57 | getInputStream(...) : InputStream | XPathExpressionTests.java:27:21:27:58 | new InputSource(...) | | XPathExpressionTests.java:27:35:27:55 | getInputStream(...) : InputStream | XPathExpressionTests.java:27:19:27:56 | new InputSource(...) |
| XPathExpressionTests.java:42:39:42:59 | getInputStream(...) : InputStream | XPathExpressionTests.java:42:23:42:60 | new InputSource(...) |
nodes nodes
| DocumentBuilderTests.java:14:19:14:39 | getInputStream(...) | semmle.label | getInputStream(...) | | DocumentBuilderTests.java:14:19:14:39 | getInputStream(...) | semmle.label | getInputStream(...) |
| DocumentBuilderTests.java:28:19:28:39 | getInputStream(...) | semmle.label | getInputStream(...) | | DocumentBuilderTests.java:28:19:28:39 | getInputStream(...) | semmle.label | getInputStream(...) |
@@ -235,8 +236,10 @@ nodes
| XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | | XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
| XMLReaderTests.java:100:18:100:55 | new InputSource(...) | semmle.label | new InputSource(...) | | XMLReaderTests.java:100:18:100:55 | new InputSource(...) | semmle.label | new InputSource(...) |
| XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | | XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
| XPathExpressionTests.java:27:21:27:58 | new InputSource(...) | semmle.label | new InputSource(...) | | XPathExpressionTests.java:27:19:27:56 | new InputSource(...) | semmle.label | new InputSource(...) |
| XPathExpressionTests.java:27:37:27:57 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | | XPathExpressionTests.java:27:35:27:55 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
| XPathExpressionTests.java:42:23:42:60 | new InputSource(...) | semmle.label | new InputSource(...) |
| XPathExpressionTests.java:42:39:42:59 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
| XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | semmle.label | getInputStream(...) | | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | semmle.label | getInputStream(...) |
| XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | semmle.label | getInputStream(...) | | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | semmle.label | getInputStream(...) |
| XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | semmle.label | getInputStream(...) | | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | semmle.label | getInputStream(...) |
@@ -336,7 +339,8 @@ subpaths
| XMLReaderTests.java:86:18:86:55 | new InputSource(...) | XMLReaderTests.java:86:34:86:54 | getInputStream(...) : InputStream | XMLReaderTests.java:86:18:86:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:86:34:86:54 | getInputStream(...) | user-provided value | | XMLReaderTests.java:86:18:86:55 | new InputSource(...) | XMLReaderTests.java:86:34:86:54 | getInputStream(...) : InputStream | XMLReaderTests.java:86:18:86:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:86:34:86:54 | getInputStream(...) | user-provided value |
| XMLReaderTests.java:94:18:94:55 | new InputSource(...) | XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | XMLReaderTests.java:94:18:94:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:94:34:94:54 | getInputStream(...) | user-provided value | | XMLReaderTests.java:94:18:94:55 | new InputSource(...) | XMLReaderTests.java:94:34:94:54 | getInputStream(...) : InputStream | XMLReaderTests.java:94:18:94:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:94:34:94:54 | getInputStream(...) | user-provided value |
| XMLReaderTests.java:100:18:100:55 | new InputSource(...) | XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | XMLReaderTests.java:100:18:100:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:100:34:100:54 | getInputStream(...) | user-provided value | | XMLReaderTests.java:100:18:100:55 | new InputSource(...) | XMLReaderTests.java:100:34:100:54 | getInputStream(...) : InputStream | XMLReaderTests.java:100:18:100:55 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XMLReaderTests.java:100:34:100:54 | getInputStream(...) | user-provided value |
| XPathExpressionTests.java:27:21:27:58 | new InputSource(...) | XPathExpressionTests.java:27:37:27:57 | getInputStream(...) : InputStream | XPathExpressionTests.java:27:21:27:58 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XPathExpressionTests.java:27:37:27:57 | getInputStream(...) | user-provided value | | XPathExpressionTests.java:27:19:27:56 | new InputSource(...) | XPathExpressionTests.java:27:35:27:55 | getInputStream(...) : InputStream | XPathExpressionTests.java:27:19:27:56 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XPathExpressionTests.java:27:35:27:55 | getInputStream(...) | user-provided value |
| XPathExpressionTests.java:42:23:42:60 | new InputSource(...) | XPathExpressionTests.java:42:39:42:59 | getInputStream(...) : InputStream | XPathExpressionTests.java:42:23:42:60 | new InputSource(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XPathExpressionTests.java:42:39:42:59 | getInputStream(...) | user-provided value |
| XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | user-provided value | | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:9:35:9:55 | getInputStream(...) | user-provided value |
| XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | user-provided value | | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:10:34:10:54 | getInputStream(...) | user-provided value |
| XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | user-provided value | | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | XML parsing depends on a $@ without guarding against external entity expansion. | XmlInputFactoryTests.java:24:35:24:55 | getInputStream(...) | user-provided value |

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved the queries for injection vulnerabilities in GitHub Actions workflows (`js/actions/command-injection` and `js/actions/pull-request-target`) and the associated library `semmle.javascript.Actions`. These now support steps defined in composite actions, in addition to steps defined in Actions workflow files. It supports more potentially untrusted input values. Additionally to the shell injections it now also detects injections in `actions/github-script`. It also detects simple injections from user controlled `${{ env.name }}`. Additionally to the `yml` extension now it also supports workflows with the `yaml` extension.

View File

@@ -12,3 +12,4 @@ dependencies:
codeql/yaml: ${workspace} codeql/yaml: ${workspace}
dataExtensions: dataExtensions:
- semmle/javascript/frameworks/**/model.yml - semmle/javascript/frameworks/**/model.yml
warnOnImplicitThis: true

View File

@@ -10,16 +10,70 @@ import javascript
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions. * See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/ */
module Actions { module Actions {
/** A YAML node in a GitHub Actions workflow file. */ /** A YAML node in a GitHub Actions workflow or a custom composite action file. */
private class Node extends YamlNode { private class Node extends YamlNode {
Node() { Node() {
this.getLocation() exists(File f |
.getFile() f = this.getLocation().getFile() and
.getRelativePath() (
.regexpMatch("(^|.*/)\\.github/workflows/.*\\.yml$") f.getRelativePath().regexpMatch("(^|.*/)\\.github/workflows/.*\\.ya?ml$")
or
f.getBaseName() = ["action.yml", "action.yaml"]
)
)
} }
} }
/**
* A custom composite action. This is a mapping at the top level of an Actions YAML action file.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions.
*/
class CompositeAction extends Node, YamlDocument, YamlMapping {
CompositeAction() {
this.getFile().getBaseName() = ["action.yml", "action.yaml"] and
this.lookup("runs").(YamlMapping).lookup("using").(YamlScalar).getValue() = "composite"
}
/** Gets the `runs` mapping. */
Runs getRuns() { result = this.lookup("runs") }
}
/**
* An `runs` mapping in a custom composite action YAML.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
*/
class Runs extends StepsContainer {
CompositeAction action;
Runs() { action.lookup("runs") = this }
/** Gets the action that this `runs` mapping is in. */
CompositeAction getAction() { result = action }
/** Gets the `using` mapping. */
Using getUsing() { result = this.lookup("using") }
}
/**
* The parent class of the class that can contain `steps` mappings. (`Job` or `Runs` currently.)
*/
abstract class StepsContainer extends YamlNode, YamlMapping {
/** Gets the sequence of `steps` within this YAML node. */
YamlSequence getSteps() { result = this.lookup("steps") }
}
/**
* A `using` mapping in a custom composite action YAML.
*/
class Using extends YamlNode, YamlScalar {
Runs runs;
Using() { runs.lookup("using") = this }
/** Gets the `runs` mapping that this `using` mapping is in. */
Runs getRuns() { result = runs }
}
/** /**
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file. * An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions. * See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
@@ -28,6 +82,9 @@ module Actions {
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */ /** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
YamlMapping getJobs() { result = this.lookup("jobs") } YamlMapping getJobs() { result = this.lookup("jobs") }
/** Gets the 'global' `env` mapping in this workflow. */
WorkflowEnv getEnv() { result = this.lookup("env") }
/** Gets the name of the workflow. */ /** Gets the name of the workflow. */
string getName() { result = this.lookup("name").(YamlString).getValue() } string getName() { result = this.lookup("name").(YamlString).getValue() }
@@ -54,11 +111,44 @@ module Actions {
Workflow getWorkflow() { result = workflow } Workflow getWorkflow() { result = workflow }
} }
/** A common class for `env` in workflow, job or step. */
abstract class Env extends YamlNode, YamlMapping { }
/** A workflow level `env` mapping. */
class WorkflowEnv extends Env {
Workflow workflow;
WorkflowEnv() { workflow.lookup("env") = this }
/** Gets the workflow this field belongs to. */
Workflow getWorkflow() { result = workflow }
}
/** A job level `env` mapping. */
class JobEnv extends Env {
Job job;
JobEnv() { job.lookup("env") = this }
/** Gets the job this field belongs to. */
Job getJob() { result = job }
}
/** A step level `env` mapping. */
class StepEnv extends Env {
Step step;
StepEnv() { step.lookup("env") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/** /**
* An Actions job within a workflow. * An Actions job within a workflow.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs. * See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
*/ */
class Job extends YamlNode, YamlMapping { class Job extends StepsContainer {
string jobId; string jobId;
Workflow workflow; Workflow workflow;
@@ -85,8 +175,8 @@ module Actions {
/** Gets the step at the given index within this job. */ /** Gets the step at the given index within this job. */
Step getStep(int index) { result.getJob() = this and result.getIndex() = index } Step getStep(int index) { result.getJob() = this and result.getIndex() = index }
/** Gets the sequence of `steps` within this job. */ /** Gets the `env` mapping in this job. */
YamlSequence getSteps() { result = this.lookup("steps") } JobEnv getEnv() { result = this.lookup("env") }
/** Gets the workflow this job belongs to. */ /** Gets the workflow this job belongs to. */
Workflow getWorkflow() { result = workflow } Workflow getWorkflow() { result = workflow }
@@ -130,15 +220,18 @@ module Actions {
*/ */
class Step extends YamlNode, YamlMapping { class Step extends YamlNode, YamlMapping {
int index; int index;
Job job; StepsContainer parent;
Step() { this = job.getSteps().getElement(index) } Step() { this = parent.getSteps().getElement(index) }
/** Gets the 0-based position of this step within the sequence of `steps`. */ /** Gets the 0-based position of this step within the sequence of `steps`. */
int getIndex() { result = index } int getIndex() { result = index }
/** Gets the job this step belongs to. */ /** Gets the `job` this step belongs to, if the step belongs to a `job` in a workflow. Has no result if the step belongs to `runs` in a custom composite action. */
Job getJob() { result = job } Job getJob() { result = parent }
/** Gets the `runs` this step belongs to, if the step belongs to a `runs` in a custom composite action. Has no result if the step belongs to a `job` in a workflow. */
Runs getRuns() { result = parent }
/** Gets the value of the `uses` field in this step, if any. */ /** Gets the value of the `uses` field in this step, if any. */
Uses getUses() { result.getStep() = this } Uses getUses() { result.getStep() = this }
@@ -149,6 +242,9 @@ module Actions {
/** Gets the value of the `if` field in this step, if any. */ /** Gets the value of the `if` field in this step, if any. */
StepIf getIf() { result.getStep() = this } StepIf getIf() { result.getStep() = this }
/** Gets the value of the `env` field in this step, if any. */
StepEnv getEnv() { result = this.lookup("env") }
/** Gets the ID of this step, if any. */ /** Gets the ID of this step, if any. */
string getId() { result = this.lookup("id").(YamlString).getValue() } string getId() { result = this.lookup("id").(YamlString).getValue() }
} }
@@ -244,6 +340,25 @@ module Actions {
With getWith() { result = with } With getWith() { result = with }
} }
/**
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
*/
string getASimpleReferenceExpression(YamlString node) {
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
// not just the last (greedy match) or first (reluctant match).
result =
node.getValue()
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\(\\)\\.\\-]+\\s*\\}\\}", _, _)
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}", 1)
}
/** Extracts the 'name' part from env.name */
bindingset[name]
string getEnvName(string name) { result = name.regexpCapture("env\\.([A-Za-z0-9_]+)", 1) }
/** /**
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell. * A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun. * See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
@@ -255,20 +370,5 @@ module Actions {
/** Gets the step that executes this `run` command. */ /** Gets the step that executes this `run` command. */
Step getStep() { result = step } Step getStep() { result = step }
/**
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this `run` command.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
*/
string getASimpleReferenceExpression() {
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
// not just the last (greedy match) or first (reluctant match).
result =
this.getValue()
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\.\\-]+\\s*\\}\\}", _, _)
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\.\\-]+)\\s*\\}\\}", 1)
}
} }
} }

View File

@@ -507,7 +507,7 @@ class DirectiveTargetName extends string {
* `:` and `_` count as component delimiters. * `:` and `_` count as component delimiters.
*/ */
string getRawComponent(int i) { string getRawComponent(int i) {
result = toLowerCase().regexpFind("(?<=^|[-:_])[a-zA-Z0-9]+(?=$|[-:_])", i, _) result = this.toLowerCase().regexpFind("(?<=^|[-:_])[a-zA-Z0-9]+(?=$|[-:_])", i, _)
} }
/** /**

View File

@@ -1,5 +1,5 @@
var cp = require("child_process"); var cp = require("child_process");
module.exports = function download(path, callback) { module.exports = function download(path, callback) {
cp.exec("wget " + path, callback); cp.execSync("wget " + path, callback);
} }

View File

@@ -1,5 +1,5 @@
var cp = require("child_process"); var cp = require("child_process");
module.exports = function download(path, callback) { module.exports = function download(path, callback) {
cp.execFile("wget", [path], callback); cp.execFileSync("wget", [path], callback);
} }

View File

@@ -9,7 +9,8 @@
</p> </p>
<p> <p>
Code injection in GitHub Actions may allow an attacker to Code injection in GitHub Actions may allow an attacker to
exfiltrate the temporary GitHub repository authorization token. exfiltrate any secrets used in the workflow and
the temporary GitHub repository authorization token.
The token might have write access to the repository, allowing an attacker The token might have write access to the repository, allowing an attacker
to use the token to make changes to the repository. to use the token to make changes to the repository.
</p> </p>
@@ -19,7 +20,8 @@
<p> <p>
The best practice to avoid code injection vulnerabilities The best practice to avoid code injection vulnerabilities
in GitHub workflows is to set the untrusted input value of the expression in GitHub workflows is to set the untrusted input value of the expression
to an intermediate environment variable. to an intermediate environment variable and then use the environment variable
using the native syntax of the shell/script interpreter (that is, not <i>${{ env.VAR }}</i>).
</p> </p>
<p> <p>
It is also recommended to limit the permissions of any tokens used It is also recommended to limit the permissions of any tokens used
@@ -33,6 +35,12 @@
</p> </p>
<sample src="examples/comment_issue_bad.yml" /> <sample src="examples/comment_issue_bad.yml" />
<p>
The following example uses an environment variable, but
<b>still allows the injection</b> because of the use of expression syntax:
</p>
<sample src="examples/comment_issue_bad_env.yml" />
<p> <p>
The following example uses shell syntax to read The following example uses shell syntax to read
the environment variable and will prevent the attack: the environment variable and will prevent the attack:

View File

@@ -15,6 +15,44 @@
import javascript import javascript
import semmle.javascript.Actions import semmle.javascript.Actions
/**
* A `script:` field within an Actions `with:` specific to `actions/github-script` action.
*
* For example:
* ```
* uses: actions/github-script@v3
* with:
* script: console.log('${{ github.event.pull_request.head.sha }}')
* ```
*/
class GitHubScript extends YamlNode, YamlString {
GitHubScriptWith with;
GitHubScript() { with.lookup("script") = this }
/** Gets the `with` field this field belongs to. */
GitHubScriptWith getWith() { result = with }
}
/**
* A step that uses `actions/github-script` action.
*/
class GitHubScriptStep extends Actions::Step {
GitHubScriptStep() { this.getUses().getGitHubRepository() = "actions/github-script" }
}
/**
* A `with:` field sibling to `uses: actions/github-script`.
*/
class GitHubScriptWith extends YamlNode, YamlMapping {
GitHubScriptStep step;
GitHubScriptWith() { step.lookup("with") = this }
/** Gets the step this field belongs to. */
GitHubScriptStep getStep() { result = step }
}
bindingset[context] bindingset[context]
private predicate isExternalUserControlledIssue(string context) { private predicate isExternalUserControlledIssue(string context) {
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*title\\b") or context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*title\\b") or
@@ -30,7 +68,10 @@ private predicate isExternalUserControlledPullRequest(string context) {
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*body\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*body\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*label\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*label\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*default_branch\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*default_branch\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*description\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*homepage\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*ref\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*ref\\b",
"\\bgithub\\s*\\.\\s*head_ref\\b"
] ]
| |
context.regexpMatch(reg) context.regexpMatch(reg)
@@ -39,8 +80,7 @@ private predicate isExternalUserControlledPullRequest(string context) {
bindingset[context] bindingset[context]
private predicate isExternalUserControlledReview(string context) { private predicate isExternalUserControlledReview(string context) {
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review\\s*\\.\\s*body\\b") or context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review\\s*\\.\\s*body\\b")
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review_comment\\s*\\.\\s*body\\b")
} }
bindingset[context] bindingset[context]
@@ -51,7 +91,8 @@ private predicate isExternalUserControlledComment(string context) {
bindingset[context] bindingset[context]
private predicate isExternalUserControlledGollum(string context) { private predicate isExternalUserControlledGollum(string context) {
context context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*page_name\\b") .regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages\\[[0-9]+\\]\\s*\\.\\s*page_name\\b") or
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages\\[[0-9]+\\]\\s*\\.\\s*title\\b")
} }
bindingset[context] bindingset[context]
@@ -59,13 +100,16 @@ private predicate isExternalUserControlledCommit(string context) {
exists(string reg | exists(string reg |
reg = reg =
[ [
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*message\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*message\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*message\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*message\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*email\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*name\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*email\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*committer\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*name\\b", "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*committer\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*head_ref\\b" "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*author\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*author\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*committer\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*committer\\s*\\.\\s*name\\b",
] ]
| |
context.regexpMatch(reg) context.regexpMatch(reg)
@@ -78,10 +122,117 @@ private predicate isExternalUserControlledDiscussion(string context) {
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*discussion\\s*\\.\\s*body\\b") context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*discussion\\s*\\.\\s*body\\b")
} }
from Actions::Run run, string context, Actions::On on bindingset[context]
private predicate isExternalUserControlledWorkflowRun(string context) {
exists(string reg |
reg =
[
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_branch\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*display_title\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_repository\\b\\s*\\.\\s*description\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*message\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*author\\b\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*author\\b\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*committer\\b\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*committer\\b\\s*\\.\\s*name\\b",
]
|
context.regexpMatch(reg)
)
}
/**
* Holds if environment name in the `injection` (in a form of `env.name`)
* is tainted by the `context` (in a form of `github.event.xxx.xxx`).
*/
bindingset[injection]
predicate isEnvInterpolationTainted(string injection, string context) {
exists(Actions::Env env, string envName, YamlString envValue |
envValue = env.lookup(envName) and
Actions::getEnvName(injection) = envName and
Actions::getASimpleReferenceExpression(envValue) = context
)
}
/**
* Holds if the `run` contains any expression interpolation `${{ e }}`.
* Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
*/
predicate isRunInjectable(Actions::Run run, string injection, string context) {
Actions::getASimpleReferenceExpression(run) = injection and
(
injection = context
or
isEnvInterpolationTainted(injection, context)
)
}
/**
* Holds if the `actions/github-script` contains any expression interpolation `${{ e }}`.
* Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
*/
predicate isScriptInjectable(GitHubScript script, string injection, string context) {
Actions::getASimpleReferenceExpression(script) = injection and
(
injection = context
or
isEnvInterpolationTainted(injection, context)
)
}
/**
* Holds if the composite action contains untrusted expression interpolation `${{ e }}`.
*/
YamlNode getInjectableCompositeActionNode(Actions::Runs runs, string injection, string context) {
exists(Actions::Run run |
isRunInjectable(run, injection, context) and
result = run and
run.getStep().getRuns() = runs
)
or
exists(GitHubScript script |
isScriptInjectable(script, injection, context) and
result = script and
script.getWith().getStep().getRuns() = runs
)
}
/**
* Holds if the workflow contains untrusted expression interpolation `${{ e }}`.
*/
YamlNode getInjectableWorkflowNode(Actions::On on, string injection, string context) {
exists(Actions::Run run |
isRunInjectable(run, injection, context) and
result = run and
run.getStep().getJob().getWorkflow().getOn() = on
)
or
exists(GitHubScript script |
isScriptInjectable(script, injection, context) and
result = script and
script.getWith().getStep().getJob().getWorkflow().getOn() = on
)
}
from YamlNode node, string injection, string context
where where
run.getASimpleReferenceExpression() = context and exists(Actions::CompositeAction action, Actions::Runs runs |
run.getStep().getJob().getWorkflow().getOn() = on and action.getRuns() = runs and
node = getInjectableCompositeActionNode(runs, injection, context) and
(
isExternalUserControlledIssue(context) or
isExternalUserControlledPullRequest(context) or
isExternalUserControlledReview(context) or
isExternalUserControlledComment(context) or
isExternalUserControlledGollum(context) or
isExternalUserControlledCommit(context) or
isExternalUserControlledDiscussion(context) or
isExternalUserControlledWorkflowRun(context)
)
)
or
exists(Actions::On on |
node = getInjectableWorkflowNode(on, injection, context) and
( (
exists(on.getNode("issues")) and exists(on.getNode("issues")) and
isExternalUserControlledIssue(context) isExternalUserControlledIssue(context)
@@ -89,21 +240,31 @@ where
exists(on.getNode("pull_request_target")) and exists(on.getNode("pull_request_target")) and
isExternalUserControlledPullRequest(context) isExternalUserControlledPullRequest(context)
or or
(exists(on.getNode("pull_request_review_comment")) or exists(on.getNode("pull_request_review"))) and exists(on.getNode("pull_request_review")) and
isExternalUserControlledReview(context) (isExternalUserControlledReview(context) or isExternalUserControlledPullRequest(context))
or or
(exists(on.getNode("issue_comment")) or exists(on.getNode("pull_request_target"))) and exists(on.getNode("pull_request_review_comment")) and
isExternalUserControlledComment(context) (isExternalUserControlledComment(context) or isExternalUserControlledPullRequest(context))
or
exists(on.getNode("issue_comment")) and
(isExternalUserControlledComment(context) or isExternalUserControlledIssue(context))
or or
exists(on.getNode("gollum")) and exists(on.getNode("gollum")) and
isExternalUserControlledGollum(context) isExternalUserControlledGollum(context)
or or
exists(on.getNode("pull_request_target")) and exists(on.getNode("push")) and
isExternalUserControlledCommit(context) isExternalUserControlledCommit(context)
or or
(exists(on.getNode("discussion")) or exists(on.getNode("discussion_comment"))) and exists(on.getNode("discussion")) and
isExternalUserControlledDiscussion(context) isExternalUserControlledDiscussion(context)
or
exists(on.getNode("discussion_comment")) and
(isExternalUserControlledDiscussion(context) or isExternalUserControlledComment(context))
or
exists(on.getNode("workflow_run")) and
isExternalUserControlledWorkflowRun(context)
) )
select run, )
"Potential injection from the " + context + select node,
" context, which may be controlled by an external user." "Potential injection from the ${{ " + injection +
" }}, which may be controlled by an external user."

View File

@@ -0,0 +1,10 @@
on: issue_comment
jobs:
echo-body:
runs-on: ubuntu-latest
steps:
- env:
BODY: ${{ github.event.issue.body }}
run: |
echo '${{ env.BODY }}'

View File

@@ -18,7 +18,7 @@
<p> <p>
This security check is, however, insufficient since an This security check is, however, insufficient since an
attacker can craft his cookie values to match those of any user. To attacker can craft their cookie values to match those of any user. To
prevent this, the server can cryptographically sign the security prevent this, the server can cryptographically sign the security
critical cookie values: critical cookie values:

View File

@@ -35,8 +35,8 @@
<p> <p>
In the example below, the untrusted value <code>req.params.id</code> is used as the property name In the example below, the untrusted value <code>req.params.id</code> is used as the property name
<code>req.session.todos[id]</code>. If a malicious user passes in the ID value <code>__proto__</code>, <code>req.session.todos[id]</code>. If a malicious user passes in the ID value <code>__proto__</code>,
the variable <code>todo</code> will then refer to <code>Object.prototype</code>. the variable <code>items</code> will then refer to <code>Object.prototype</code>.
Finally, the modification of <code>todo</code> then allows the attacker to inject arbitrary properties Finally, the modification of <code>items</code> then allows the attacker to inject arbitrary properties
onto <code>Object.prototype</code>. onto <code>Object.prototype</code>.
</p> </p>
@@ -49,6 +49,12 @@
<sample src="examples/PrototypePollutingAssignmentFixed.js"/> <sample src="examples/PrototypePollutingAssignmentFixed.js"/>
<p>
Another way to fix it is to prevent the <code>__proto__</code> property from being used as a key, as shown below:
</p>
<sample src="examples/PrototypePollutingAssignmentFixed2.js"/>
</example> </example>
<references> <references>

View File

@@ -0,0 +1,16 @@
let express = require('express');
let app = express()
app.put('/todos/:id', (req, res) => {
let id = req.params.id;
if (id === '__proto__' || id === 'constructor' || id === 'prototype') {
res.end(403);
return;
}
let items = req.session.todos[id];
if (!items) {
items = req.session.todos[id] = {};
}
items[req.query.name] = req.query.text;
res.end(200);
});

View File

@@ -17,7 +17,7 @@ import javascript
import semmle.javascript.Actions import semmle.javascript.Actions
/** /**
* An action step that doesn't contain `actor` or `label` check in `if:` or * An action step that doesn't contain `actor` check in `if:` or
* the check requires manual analysis. * the check requires manual analysis.
*/ */
class ProbableStep extends Actions::Step { class ProbableStep extends Actions::Step {
@@ -29,25 +29,13 @@ class ProbableStep extends Actions::Step {
// needs manual analysis if there is OR // needs manual analysis if there is OR
this.getIf().getValue().matches("%||%") this.getIf().getValue().matches("%||%")
or or
// labels can be assigned by owners only
not exists(
this.getIf()
.getValue()
.regexpFind("\\bcontains\\s*\\(\\s*github\\s*\\.\\s*event\\s*\\.\\s*(?:issue|pull_request)\\s*\\.\\s*labels\\b",
_, _)
) and
not exists(
this.getIf()
.getValue()
.regexpFind("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*label\\s*\\.\\s*name\\s*==", _, _)
) and
// actor check means only the user is able to run it // actor check means only the user is able to run it
not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _)) not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _))
} }
} }
/** /**
* An action job that doesn't contain `actor` or `label` check in `if:` or * An action job that doesn't contain `actor` check in `if:` or
* the check requires manual analysis. * the check requires manual analysis.
*/ */
class ProbableJob extends Actions::Job { class ProbableJob extends Actions::Job {
@@ -59,46 +47,18 @@ class ProbableJob extends Actions::Job {
// needs manual analysis if there is OR // needs manual analysis if there is OR
this.getIf().getValue().matches("%||%") this.getIf().getValue().matches("%||%")
or or
// labels can be assigned by owners only
not exists(
this.getIf()
.getValue()
.regexpFind("\\bcontains\\s*\\(\\s*github\\s*\\.\\s*event\\s*\\.\\s*(?:issue|pull_request)\\s*\\.\\s*labels\\b",
_, _)
) and
not exists(
this.getIf()
.getValue()
.regexpFind("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*label\\s*\\.\\s*name\\s*==", _, _)
) and
// actor check means only the user is able to run it // actor check means only the user is able to run it
not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _)) not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _))
} }
} }
/** /**
* An action step that doesn't contain `actor` or `label` check in `if:` or * The `on: pull_request_target`.
*/ */
class ProbablePullRequestTarget extends Actions::On, YamlMappingLikeNode { class ProbablePullRequestTarget extends Actions::On, YamlMappingLikeNode {
ProbablePullRequestTarget() { ProbablePullRequestTarget() {
exists(YamlNode prtNode |
// The `on:` is triggered on `pull_request_target` // The `on:` is triggered on `pull_request_target`
this.getNode("pull_request_target") = prtNode and exists(this.getNode("pull_request_target"))
(
// and either doesn't contain `types` filter
not exists(prtNode.getAChild())
or
// or has the filter, that is something else than just [labeled]
exists(YamlMappingLikeNode prt, YamlMappingLikeNode types |
types = prt.getNode("types") and
prtNode = prt and
(
not types.getElementCount() = 1 or
not exists(types.getNode("labeled"))
)
)
)
)
} }
} }

View File

@@ -11,3 +11,4 @@ dependencies:
codeql/suite-helpers: ${workspace} codeql/suite-helpers: ${workspace}
codeql/typos: ${workspace} codeql/typos: ${workspace}
codeql/util: ${workspace} codeql/util: ${workspace}
warnOnImplicitThis: true

View File

@@ -1,7 +1,13 @@
| .github/workflows/pull_request_target_if_job.yml:9:7:12:2 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_job.yml:16:7:19:2 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_job.yml:30:7:33:2 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_if_job.yml:30:7:33:2 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_job.yml:36:7:38:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_if_job.yml:36:7:38:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_step.yml:9:7:14:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_step.yml:14:7:19:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_step.yml:24:7:29:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_if_step.yml:24:7:29:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_if_step.yml:29:7:31:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_if_step.yml:29:7:31:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_label_only.yml:10:7:12:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_label_only_mapping.yml:11:7:13:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_labels_mapping.yml:13:7:15:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_labels_mapping.yml:13:7:15:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_labels_sequence.yml:10:7:12:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_labels_sequence.yml:10:7:12:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |
| .github/workflows/pull_request_target_mapping.yml:8:7:10:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. | | .github/workflows/pull_request_target_mapping.yml:8:7:10:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on 'pull_request_target'. |

View File

@@ -1 +0,0 @@
console.log('test')

View File

@@ -7,3 +7,4 @@ extractor: javascript
tests: . tests: .
dataExtensions: dataExtensions:
- library-tests/DataExtensions/*.model.yml - library-tests/DataExtensions/*.model.yml
warnOnImplicitThis: true

View File

@@ -10,5 +10,19 @@ jobs:
echo-chamber2: echo-chamber2:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: | - run: echo '${{ github.event.comment.body }}'
echo '${{ github.event.comment.body }}' - run: echo '${{ github.event.issue.body }}'
- run: echo '${{ github.event.issue.title }}'
echo-chamber3:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
script: console.log('${{ github.event.comment.body }}')
- uses: actions/github-script@v3
with:
script: console.log('${{ github.event.issue.body }}')
- uses: actions/github-script@v3
with:
script: console.log('${{ github.event.issue.title }}')

View File

@@ -0,0 +1,8 @@
on: discussion
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.discussion.title }}'
- run: echo '${{ github.event.discussion.body }}'

View File

@@ -0,0 +1,9 @@
on: discussion_comment
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.discussion.title }}'
- run: echo '${{ github.event.discussion.body }}'
- run: echo '${{ github.event.comment.body }}'

View File

@@ -0,0 +1,11 @@
on: gollum
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.pages[1].title }}'
- run: echo '${{ github.event.pages[11].title }}'
- run: echo '${{ github.event.pages[0].page_name }}'
- run: echo '${{ github.event.pages[2222].page_name }}'
- run: echo '${{ toJSON(github.event.pages.*.title) }}' # safe

View File

@@ -0,0 +1,20 @@
on: issues
env:
global_env: ${{ github.event.issue.title }}
test: test
jobs:
echo-chamber:
env:
job_env: ${{ github.event.issue.title }}
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.issue.title }}'
- run: echo '${{ github.event.issue.body }}'
- run: echo '${{ env.global_env }}'
- run: echo '${{ env.test }}'
- run: echo '${{ env.job_env }}'
- run: echo '${{ env.step_env }}'
env:
step_env: ${{ github.event.issue.title }}

View File

@@ -0,0 +1,14 @@
on: pull_request_review
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.pull_request.title }}'
- run: echo '${{ github.event.pull_request.body }}'
- run: echo '${{ github.event.pull_request.head.label }}'
- run: echo '${{ github.event.pull_request.head.repo.default_branch }}'
- run: echo '${{ github.event.pull_request.head.repo.description }}'
- run: echo '${{ github.event.pull_request.head.repo.homepage }}'
- run: echo '${{ github.event.pull_request.head.ref }}'
- run: echo '${{ github.event.review.body }}'

View File

@@ -0,0 +1,14 @@
on: pull_request_review_comment
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.pull_request.title }}'
- run: echo '${{ github.event.pull_request.body }}'
- run: echo '${{ github.event.pull_request.head.label }}'
- run: echo '${{ github.event.pull_request.head.repo.default_branch }}'
- run: echo '${{ github.event.pull_request.head.repo.description }}'
- run: echo '${{ github.event.pull_request.head.repo.homepage }}'
- run: echo '${{ github.event.pull_request.head.ref }}'
- run: echo '${{ github.event.comment.body }}'

View File

@@ -0,0 +1,16 @@
on: pull_request_target
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.issue.title }}' # not defined
- run: echo '${{ github.event.issue.body }}' # not defined
- run: echo '${{ github.event.pull_request.title }}'
- run: echo '${{ github.event.pull_request.body }}'
- run: echo '${{ github.event.pull_request.head.label }}'
- run: echo '${{ github.event.pull_request.head.repo.default_branch }}'
- run: echo '${{ github.event.pull_request.head.repo.description }}'
- run: echo '${{ github.event.pull_request.head.repo.homepage }}'
- run: echo '${{ github.event.pull_request.head.ref }}'
- run: echo '${{ github.head_ref }}'

View File

@@ -0,0 +1,16 @@
on: push
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.commits[11].message }}'
- run: echo '${{ github.event.commits[11].author.email }}'
- run: echo '${{ github.event.commits[11].author.name }}'
- run: echo '${{ github.event.head_commit.message }}'
- run: echo '${{ github.event.head_commit.author.email }}'
- run: echo '${{ github.event.head_commit.author.name }}'
- run: echo '${{ github.event.head_commit.committer.email }}'
- run: echo '${{ github.event.head_commit.committer.name }}'
- run: echo '${{ github.event.commits[11].committer.email }}'
- run: echo '${{ github.event.commits[11].committer.name }}'

View File

@@ -0,0 +1,16 @@
on:
workflow_run:
workflows: [test]
jobs:
echo-chamber:
runs-on: ubuntu-latest
steps:
- run: echo '${{ github.event.workflow_run.display_title }}'
- run: echo '${{ github.event.workflow_run.head_commit.message }}'
- run: echo '${{ github.event.workflow_run.head_commit.author.email }}'
- run: echo '${{ github.event.workflow_run.head_commit.author.name }}'
- run: echo '${{ github.event.workflow_run.head_commit.committer.email }}'
- run: echo '${{ github.event.workflow_run.head_commit.committer.name }}'
- run: echo '${{ github.event.workflow_run.head_branch }}'
- run: echo '${{ github.event.workflow_run.head_repository.description }}'

View File

@@ -1,3 +1,65 @@
| .github/workflows/comment_issue.yml:7:12:8:48 | \| | Potential injection from the github.event.comment.body context, which may be controlled by an external user. | | .github/workflows/comment_issue.yml:7:12:8:48 | \| | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/comment_issue.yml:13:12:14:47 | \| | Potential injection from the github.event.comment.body context, which may be controlled by an external user. | | .github/workflows/comment_issue.yml:13:12:13:50 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/comment_issue_newline.yml:9:14:10:50 | \| | Potential injection from the github.event.comment.body context, which may be controlled by an external user. | | .github/workflows/comment_issue.yml:14:12:14:48 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.issue.body }}, which may be controlled by an external user. |
| .github/workflows/comment_issue.yml:15:12:15:49 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.issue.title }}, which may be controlled by an external user. |
| .github/workflows/comment_issue.yml:22:17:22:63 | console ... dy }}') | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/comment_issue.yml:25:17:25:61 | console ... dy }}') | Potential injection from the ${{ github.event.issue.body }}, which may be controlled by an external user. |
| .github/workflows/comment_issue.yml:28:17:28:62 | console ... le }}') | Potential injection from the ${{ github.event.issue.title }}, which may be controlled by an external user. |
| .github/workflows/comment_issue_newline.yml:9:14:10:50 | \| | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/discussion.yml:7:12:7:54 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.discussion.title }}, which may be controlled by an external user. |
| .github/workflows/discussion.yml:8:12:8:53 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.discussion.body }}, which may be controlled by an external user. |
| .github/workflows/discussion_comment.yml:7:12:7:54 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.discussion.title }}, which may be controlled by an external user. |
| .github/workflows/discussion_comment.yml:8:12:8:53 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.discussion.body }}, which may be controlled by an external user. |
| .github/workflows/discussion_comment.yml:9:12:9:50 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/gollum.yml:7:12:7:52 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.pages[1].title }}, which may be controlled by an external user. |
| .github/workflows/gollum.yml:8:12:8:53 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.pages[11].title }}, which may be controlled by an external user. |
| .github/workflows/gollum.yml:9:12:9:56 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.pages[0].page_name }}, which may be controlled by an external user. |
| .github/workflows/gollum.yml:10:12:10:59 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.pages[2222].page_name }}, which may be controlled by an external user. |
| .github/workflows/issues.yaml:13:12:13:49 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.issue.title }}, which may be controlled by an external user. |
| .github/workflows/issues.yaml:14:12:14:48 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.issue.body }}, which may be controlled by an external user. |
| .github/workflows/issues.yaml:15:12:15:39 | echo '$ ... env }}' | Potential injection from the ${{ env.global_env }}, which may be controlled by an external user. |
| .github/workflows/issues.yaml:17:12:17:36 | echo '$ ... env }}' | Potential injection from the ${{ env.job_env }}, which may be controlled by an external user. |
| .github/workflows/issues.yaml:18:12:18:37 | echo '$ ... env }}' | Potential injection from the ${{ env.step_env }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:7:12:7:56 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.pull_request.title }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:8:12:8:55 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.pull_request.body }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:9:12:9:61 | echo '$ ... bel }}' | Potential injection from the ${{ github.event.pull_request.head.label }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:10:12:10:75 | echo '$ ... nch }}' | Potential injection from the ${{ github.event.pull_request.head.repo.default_branch }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:11:12:11:72 | echo '$ ... ion }}' | Potential injection from the ${{ github.event.pull_request.head.repo.description }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:12:12:12:69 | echo '$ ... age }}' | Potential injection from the ${{ github.event.pull_request.head.repo.homepage }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:13:12:13:59 | echo '$ ... ref }}' | Potential injection from the ${{ github.event.pull_request.head.ref }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review.yml:14:12:14:49 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.review.body }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:7:12:7:56 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.pull_request.title }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:8:12:8:55 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.pull_request.body }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:9:12:9:61 | echo '$ ... bel }}' | Potential injection from the ${{ github.event.pull_request.head.label }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:10:12:10:75 | echo '$ ... nch }}' | Potential injection from the ${{ github.event.pull_request.head.repo.default_branch }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:11:12:11:72 | echo '$ ... ion }}' | Potential injection from the ${{ github.event.pull_request.head.repo.description }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:12:12:12:69 | echo '$ ... age }}' | Potential injection from the ${{ github.event.pull_request.head.repo.homepage }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:13:12:13:59 | echo '$ ... ref }}' | Potential injection from the ${{ github.event.pull_request.head.ref }}, which may be controlled by an external user. |
| .github/workflows/pull_request_review_comment.yml:14:12:14:50 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:9:12:9:56 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.pull_request.title }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:10:12:10:55 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.pull_request.body }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:11:12:11:61 | echo '$ ... bel }}' | Potential injection from the ${{ github.event.pull_request.head.label }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:12:12:12:75 | echo '$ ... nch }}' | Potential injection from the ${{ github.event.pull_request.head.repo.default_branch }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:13:12:13:72 | echo '$ ... ion }}' | Potential injection from the ${{ github.event.pull_request.head.repo.description }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:14:12:14:69 | echo '$ ... age }}' | Potential injection from the ${{ github.event.pull_request.head.repo.homepage }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:15:12:15:59 | echo '$ ... ref }}' | Potential injection from the ${{ github.event.pull_request.head.ref }}, which may be controlled by an external user. |
| .github/workflows/pull_request_target.yml:16:12:16:40 | echo '$ ... ref }}' | Potential injection from the ${{ github.head_ref }}, which may be controlled by an external user. |
| .github/workflows/push.yml:7:12:7:57 | echo '$ ... age }}' | Potential injection from the ${{ github.event.commits[11].message }}, which may be controlled by an external user. |
| .github/workflows/push.yml:8:12:8:62 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.commits[11].author.email }}, which may be controlled by an external user. |
| .github/workflows/push.yml:9:12:9:61 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.commits[11].author.name }}, which may be controlled by an external user. |
| .github/workflows/push.yml:10:12:10:57 | echo '$ ... age }}' | Potential injection from the ${{ github.event.head_commit.message }}, which may be controlled by an external user. |
| .github/workflows/push.yml:11:12:11:62 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.head_commit.author.email }}, which may be controlled by an external user. |
| .github/workflows/push.yml:12:12:12:61 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.head_commit.author.name }}, which may be controlled by an external user. |
| .github/workflows/push.yml:13:12:13:65 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.head_commit.committer.email }}, which may be controlled by an external user. |
| .github/workflows/push.yml:14:12:14:64 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.head_commit.committer.name }}, which may be controlled by an external user. |
| .github/workflows/push.yml:15:12:15:65 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.commits[11].committer.email }}, which may be controlled by an external user. |
| .github/workflows/push.yml:16:12:16:64 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.commits[11].committer.name }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:9:12:9:64 | echo '$ ... tle }}' | Potential injection from the ${{ github.event.workflow_run.display_title }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:10:12:10:70 | echo '$ ... age }}' | Potential injection from the ${{ github.event.workflow_run.head_commit.message }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:11:12:11:75 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.workflow_run.head_commit.author.email }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:12:12:12:74 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.workflow_run.head_commit.author.name }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:13:12:13:78 | echo '$ ... ail }}' | Potential injection from the ${{ github.event.workflow_run.head_commit.committer.email }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:14:12:14:77 | echo '$ ... ame }}' | Potential injection from the ${{ github.event.workflow_run.head_commit.committer.name }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:15:12:15:62 | echo '$ ... nch }}' | Potential injection from the ${{ github.event.workflow_run.head_branch }}, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:16:12:16:78 | echo '$ ... ion }}' | Potential injection from the ${{ github.event.workflow_run.head_repository.description }}, which may be controlled by an external user. |
| action1/action.yml:14:12:14:50 | echo '$ ... ody }}' | Potential injection from the ${{ github.event.comment.body }}, which may be controlled by an external user. |

View File

@@ -0,0 +1,14 @@
name: 'test'
description: 'test'
branding:
icon: 'test'
color: 'test'
inputs:
test:
description: test
required: false
default: 'test'
runs:
using: "composite"
steps:
- run: echo '${{ github.event.comment.body }}'

View File

@@ -0,0 +1,17 @@
name: 'Hello World'
description: 'Greet someone and record the time'
inputs:
who-to-greet: # id of input
description: 'Who to greet'
required: true
default: 'World'
outputs:
time: # id of output
description: 'The time we greeted you'
runs:
using: 'docker'
steps: # this is actually invalid, used to test we correctly identify composite actions
- run: echo '${{ github.event.comment.body }}'
image: 'Dockerfile'
args:
- ${{ inputs.who-to-greet }}

View File

@@ -34,7 +34,7 @@ class RamlResource extends YamlMapping {
/** Get the method for this resource with the given verb. */ /** Get the method for this resource with the given verb. */
RamlMethod getMethod(string verb) { RamlMethod getMethod(string verb) {
verb = httpVerb() and verb = httpVerb() and
result = lookup(verb) result = this.lookup(verb)
} }
} }

View File

@@ -2,7 +2,7 @@ import javascript
/** A RAML specification. */ /** A RAML specification. */
class RamlSpec extends YamlDocument, YamlMapping { class RamlSpec extends YamlDocument, YamlMapping {
RamlSpec() { getLocation().getFile().getExtension() = "raml" } RamlSpec() { this.getLocation().getFile().getExtension() = "raml" }
} }
from RamlSpec s from RamlSpec s

View File

@@ -4,13 +4,13 @@ string httpVerb() { result = ["get", "put", "post", "delete"] }
/** A RAML specification. */ /** A RAML specification. */
class RamlSpec extends YamlDocument, YamlMapping { class RamlSpec extends YamlDocument, YamlMapping {
RamlSpec() { getLocation().getFile().getExtension() = "raml" } RamlSpec() { this.getLocation().getFile().getExtension() = "raml" }
} }
/** A RAML resource specification. */ /** A RAML resource specification. */
class RamlResource extends YamlMapping { class RamlResource extends YamlMapping {
RamlResource() { RamlResource() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping m, string name | exists(YamlMapping m, string name |
this = m.lookup(name) and this = m.lookup(name) and
name.matches("/%") name.matches("/%")
@@ -30,14 +30,14 @@ class RamlResource extends YamlMapping {
/** Get the method for this resource with the given verb. */ /** Get the method for this resource with the given verb. */
RamlMethod getMethod(string verb) { RamlMethod getMethod(string verb) {
verb = httpVerb() and verb = httpVerb() and
result = lookup(verb) result = this.lookup(verb)
} }
} }
/** A RAML method specification. */ /** A RAML method specification. */
class RamlMethod extends YamlValue { class RamlMethod extends YamlValue {
RamlMethod() { RamlMethod() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping obj | this = obj.lookup(httpVerb())) exists(YamlMapping obj | this = obj.lookup(httpVerb()))
} }

View File

@@ -4,13 +4,13 @@ string httpVerb() { result = ["get", "put", "post", "delete"] }
/** A RAML specification. */ /** A RAML specification. */
class RamlSpec extends YamlDocument, YamlMapping { class RamlSpec extends YamlDocument, YamlMapping {
RamlSpec() { getLocation().getFile().getExtension() = "raml" } RamlSpec() { this.getLocation().getFile().getExtension() = "raml" }
} }
/** A RAML resource specification. */ /** A RAML resource specification. */
class RamlResource extends YamlMapping { class RamlResource extends YamlMapping {
RamlResource() { RamlResource() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping m, string name | exists(YamlMapping m, string name |
this = m.lookup(name) and this = m.lookup(name) and
name.matches("/%") name.matches("/%")
@@ -30,13 +30,13 @@ class RamlResource extends YamlMapping {
/** Get the method for this resource with the given verb. */ /** Get the method for this resource with the given verb. */
RamlMethod getMethod(string verb) { RamlMethod getMethod(string verb) {
verb = httpVerb() and verb = httpVerb() and
result = lookup(verb) result = this.lookup(verb)
} }
} }
class RamlMethod extends YamlValue { class RamlMethod extends YamlValue {
RamlMethod() { RamlMethod() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping obj | this = obj.lookup(httpVerb())) exists(YamlMapping obj | this = obj.lookup(httpVerb()))
} }
} }

View File

@@ -4,13 +4,13 @@ string httpVerb() { result = ["get", "put", "post", "delete"] }
/** A RAML specification. */ /** A RAML specification. */
class RamlSpec extends YamlDocument, YamlMapping { class RamlSpec extends YamlDocument, YamlMapping {
RamlSpec() { getLocation().getFile().getExtension() = "raml" } RamlSpec() { this.getLocation().getFile().getExtension() = "raml" }
} }
/** A RAML resource specification. */ /** A RAML resource specification. */
class RamlResource extends YamlMapping { class RamlResource extends YamlMapping {
RamlResource() { RamlResource() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping m, string name | exists(YamlMapping m, string name |
this = m.lookup(name) and this = m.lookup(name) and
name.matches("/%") name.matches("/%")
@@ -30,14 +30,14 @@ class RamlResource extends YamlMapping {
/** Get the method for this resource with the given verb. */ /** Get the method for this resource with the given verb. */
RamlMethod getMethod(string verb) { RamlMethod getMethod(string verb) {
verb = httpVerb() and verb = httpVerb() and
result = lookup(verb) result = this.lookup(verb)
} }
} }
/** A RAML method specification. */ /** A RAML method specification. */
class RamlMethod extends YamlValue { class RamlMethod extends YamlValue {
RamlMethod() { RamlMethod() {
getDocument() instanceof RamlSpec and this.getDocument() instanceof RamlSpec and
exists(YamlMapping obj | this = obj.lookup(httpVerb())) exists(YamlMapping obj | this = obj.lookup(httpVerb()))
} }

View File

@@ -28,6 +28,27 @@ module SummaryComponent {
/** Gets a summary component that represents a list element. */ /** Gets a summary component that represents a list element. */
SummaryComponent listElement() { result = content(any(ListElementContent c)) } SummaryComponent listElement() { result = content(any(ListElementContent c)) }
/** Gets a summary component that represents a set element. */
SummaryComponent setElement() { result = content(any(SetElementContent c)) }
/** Gets a summary component that represents a tuple element. */
SummaryComponent tupleElement(int index) {
exists(TupleElementContent c | c.getIndex() = index and result = content(c))
}
/** Gets a summary component that represents a dictionary element. */
SummaryComponent dictionaryElement(string key) {
exists(DictionaryElementContent c | c.getKey() = key and result = content(c))
}
/** Gets a summary component that represents a dictionary element at any key. */
SummaryComponent dictionaryElementAny() { result = content(any(DictionaryElementAnyContent c)) }
/** Gets a summary component that represents an attribute element. */
SummaryComponent attribute(string attr) {
exists(AttributeContent c | c.getAttribute() = attr and result = content(c))
}
/** Gets a summary component that represents the return value of a call. */ /** Gets a summary component that represents the return value of a call. */
SummaryComponent return() { result = SC::return(any(ReturnKind rk)) } SummaryComponent return() { result = SC::return(any(ReturnKind rk)) }
} }

View File

@@ -105,6 +105,27 @@ predicate neutralSummaryElement(FlowSummary::SummarizedCallable c, string proven
SummaryComponent interpretComponentSpecific(AccessPathToken c) { SummaryComponent interpretComponentSpecific(AccessPathToken c) {
c = "ListElement" and c = "ListElement" and
result = FlowSummary::SummaryComponent::listElement() result = FlowSummary::SummaryComponent::listElement()
or
c = "SetElement" and
result = FlowSummary::SummaryComponent::setElement()
or
exists(int index |
c.getAnArgument("TupleElement") = index.toString() and
result = FlowSummary::SummaryComponent::tupleElement(index)
)
or
exists(string key |
c.getAnArgument("DictionaryElement") = key and
result = FlowSummary::SummaryComponent::dictionaryElement(key)
)
or
c = "DictionaryElementAny" and
result = FlowSummary::SummaryComponent::dictionaryElementAny()
or
exists(string attr |
c.getAnArgument("Attribute") = attr and
result = FlowSummary::SummaryComponent::attribute(attr)
)
} }
/** Gets the textual representation of a summary component in the format used for flow summaries. */ /** Gets the textual representation of a summary component in the format used for flow summaries. */

View File

@@ -613,8 +613,17 @@ class TypeBackTracker extends TTypeBackTracker {
* also flow to `sink`. * also flow to `sink`.
*/ */
TypeTracker getACompatibleTypeTracker() { TypeTracker getACompatibleTypeTracker() {
exists(boolean hasCall | result = MkTypeTracker(hasCall, content) | exists(boolean hasCall, OptionalTypeTrackerContent c |
hasCall = false or this.hasReturn() = false result = MkTypeTracker(hasCall, c) and
(
compatibleContents(c, content)
or
content = noContent() and c = content
)
|
hasCall = false
or
this.hasReturn() = false
) )
} }
} }

View File

@@ -0,0 +1,357 @@
# This tests some of the common built-in functions and methods.
# We need a decent model of data flow through these in order to
# analyse most programs.
#
# All functions starting with "test_" should run and execute `print("OK")` exactly once.
# This can be checked by running validTest.py.
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname((__file__))))
from testlib import expects
# These are defined so that we can evaluate the test code.
NONSOURCE = "not a source"
SOURCE = "source"
def is_source(x):
return x == "source" or x == b"source" or x == 42 or x == 42.0 or x == 42j
def SINK(x):
if is_source(x):
print("OK")
else:
print("Unexpected flow", x)
def SINK_F(x):
if is_source(x):
print("Unexpected flow", x)
else:
print("OK")
# Actual tests
## Container constructors
### List
@expects(2)
def test_list_from_list():
l1 = [SOURCE, NONSOURCE]
l2 = list(l1)
SINK(l2[0]) #$ MISSING: flow="SOURCE, l:-2 -> l2[0]"
SINK_F(l2[1]) # expecting FP due to imprecise flow
# -- skip list_from_string
@expects(2)
def test_list_from_tuple():
t = (SOURCE, NONSOURCE)
l = list(t)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
SINK_F(l[1]) # expecting FP due to imprecise flow
def test_list_from_set():
s = {SOURCE}
l = list(s)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
@expects(2)
def test_list_from_dict():
d = {SOURCE: 'v', NONSOURCE: 'v2'}
l = list(d)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
SINK_F(l[1]) # expecting FP due to imprecise flow
### Tuple
@expects(2)
def test_tuple_from_list():
l = [SOURCE, NONSOURCE]
t = tuple(l)
SINK(t[0]) #$ MISSING: flow="SOURCE, l:-2 -> t[0]"
SINK_F(t[1])
@expects(2)
def test_tuple_from_tuple():
t0 = (SOURCE, NONSOURCE)
t = tuple(t0)
SINK(t[0]) #$ MISSING: flow="SOURCE, l:-2 -> t[0]"
SINK_F(t[1])
def test_tuple_from_set():
s = {SOURCE}
t = tuple(s)
SINK(t[0]) #$ MISSING: flow="SOURCE, l:-2 -> t[0]"
@expects(2)
def test_tuple_from_dict():
d = {SOURCE: "v1", NONSOURCE: "v2"}
t = tuple(d)
SINK(t[0]) #$ MISSING: flow="SOURCE, l:-2 -> t[0]"
SINK_F(t[1])
### Set
def test_set_from_list():
l = [SOURCE]
s = set(l)
v = s.pop()
SINK(v) #$ MISSING: flow="SOURCE, l:-3 -> v"
def test_set_from_tuple():
t = (SOURCE,)
s = set(t)
v = s.pop()
SINK(v) #$ MISSING: flow="SOURCE, l:-3 -> v"
def test_set_from_set():
s0 = {SOURCE}
s = set(s0)
v = s.pop()
SINK(v) #$ MISSING: flow="SOURCE, l:-3 -> v"
def test_set_from_dict():
d = {SOURCE: "val"}
s = set(d)
v = s.pop()
SINK(v) #$ MISSING: flow="SOURCE, l:-3 -> v"
### Dict
@expects(2)
def test_dict_from_keyword():
d = dict(k = SOURCE, k1 = NONSOURCE)
SINK(d["k"]) #$ MISSING: flow="SOURCE, l:-1 -> d[k]"
SINK_F(d["k1"])
@expects(2)
def test_dict_from_list():
d = dict([("k", SOURCE), ("k1", NONSOURCE)])
SINK(d["k"]) #$ MISSING: flow="SOURCE, l:-1 -> d[k]"
SINK_F(d["k1"])
@expects(2)
def test_dict_from_dict():
d1 = {'k': SOURCE, 'k1': NONSOURCE}
d2 = dict(d1)
SINK(d2["k"]) #$ MISSING: flow="SOURCE, l:-2 -> d[k]"
SINK_F(d2["k1"])
## Container methods
### List
def test_list_pop():
l = [SOURCE]
v = l.pop()
SINK(v) #$ flow="SOURCE, l:-2 -> v"
def test_list_pop_index():
l = [SOURCE]
v = l.pop(0)
SINK(v) #$ MISSING: flow="SOURCE, l:-2 -> v"
def test_list_pop_index_imprecise():
l = [SOURCE, NONSOURCE]
v = l.pop(1)
SINK_F(v)
@expects(2)
def test_list_copy():
l0 = [SOURCE, NONSOURCE]
l = l0.copy()
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
SINK_F(l[1])
def test_list_append():
l = [NONSOURCE]
l.append(SOURCE)
SINK(l[1]) #$ MISSING: flow="SOURCE, l:-1 -> l[1]"
### Set
def test_set_pop():
s = {SOURCE}
v = s.pop()
SINK(v) #$ flow="SOURCE, l:-2 -> v"
def test_set_copy():
s0 = {SOURCE}
s = s0.copy()
SINK(s.pop()) #$ MISSING: flow="SOURCE, l:-2 -> s.pop()"
def test_set_add():
s = set([])
s.add(SOURCE)
SINK(s.pop()) #$ MISSING: flow="SOURCE, l:-2 -> s.pop()"
### Dict
def test_dict_keys():
d = {SOURCE: "value"}
keys = d.keys()
key_list = list(keys)
SINK(key_list[0]) #$ MISSING: flow="SOURCE, l:-3 -> key_list[0]"
def test_dict_values():
d = {'k': SOURCE}
vals = d.values()
val_list = list(vals)
SINK(val_list[0]) #$ MISSING: flow="SOURCE, l:-3 -> val_list[0]"
@expects(4)
def test_dict_items():
d = {'k': SOURCE, SOURCE: "value"}
items = d.items()
item_list = list(items)
SINK_F(item_list[0][0]) # expecting FP due to imprecise flow
SINK(item_list[0][1]) #$ MISSING: flow="SOURCE, l:-4 -> item_list[0][1]"
SINK(item_list[1][0]) #$ MISSING: flow="SOURCE, l:-5 -> item_list[1][0]"
SINK_F(item_list[1][1]) # expecting FP due to imprecise flow
@expects(3)
def test_dict_pop():
d = {'k': SOURCE}
v = d.pop("k")
SINK(v) #$ flow="SOURCE, l:-2 -> v"
v1 = d.pop("k", NONSOURCE)
SINK_F(v1) #$ SPURIOUS: flow="SOURCE, l:-4 -> v1"
v2 = d.pop("non-existing", SOURCE)
SINK(v2) #$ MISSING: flow="SOURCE, l:-1 -> v2"
@expects(2)
def test_dict_get():
d = {'k': SOURCE}
v = d.get("k")
SINK(v) #$ flow="SOURCE, l:-2 -> v"
v1 = d.get("non-existing", SOURCE)
SINK(v1) #$ MISSING: flow="SOURCE, l:-1 -> v1"
@expects(2)
def test_dict_popitem():
d = {'k': SOURCE}
t = d.popitem() # could be any pair (before 3.7), but we only have one
SINK_F(t[0])
SINK(t[1]) #$ MISSING: flow="SOURCE, l:-3 -> t[1]"
@expects(2)
def test_dict_copy():
d = {'k': SOURCE, 'k1': NONSOURCE}
d1 = d.copy()
SINK(d1["k"]) #$ MISSING: flow="SOURCE, l:-2 -> d[k]"
SINK_F(d1["k1"])
## Functions on containers
### sorted
def test_sorted_list():
l0 = [SOURCE]
l = sorted(l0)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
def test_sorted_tuple():
t = (SOURCE,)
l = sorted(t)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
def test_sorted_set():
s = {SOURCE}
l = sorted(s)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
def test_sorted_dict():
d = {SOURCE: "val"}
l = sorted(d)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-2 -> l[0]"
### reversed
@expects(2)
def test_reversed_list():
l0 = [SOURCE, NONSOURCE]
r = reversed(l0)
l = list(r)
SINK_F(l[0])
SINK(l[1]) #$ MISSING: flow="SOURCE, l:-4 -> l[1]"
@expects(2)
def test_reversed_tuple():
t = (SOURCE, NONSOURCE)
r = reversed(t)
l = list(r)
SINK_F(l[0])
SINK(l[1]) #$ MISSING: flow="SOURCE, l:-4 -> l[1]"
@expects(2)
def test_reversed_dict():
d = {SOURCE: "v1", NONSOURCE: "v2"}
r = reversed(d)
l = list(r)
SINK_F(l[0])
SINK(l[1]) #$ MISSING: flow="SOURCE, l:-4 -> l[1]"
### iter
def test_iter_list():
l0 = [SOURCE]
i = iter(l0)
l = list(i)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-3 -> l[0]"
def test_iter_tuple():
t = (SOURCE,)
i = iter(t)
l = list(i)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-3 -> l[0]"
def test_iter_set():
t = {SOURCE}
i = iter(t)
l = list(i)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-3 -> l[0]"
def test_iter_dict():
d = {SOURCE: "val"}
i = iter(d)
l = list(i)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-3 -> l[0]"
def test_iter_iter():
# applying iter() to the result of iter() is basically a no-op
l0 = [SOURCE]
i = iter(iter(l0))
l = list(i)
SINK(l[0]) #$ MISSING: flow="SOURCE, l:-3 -> l[0]"
### next
def test_next_list():
l = [SOURCE]
i = iter(l)
n = next(i)
SINK(n) #$ MISSING: flow="SOURCE, l:-3 -> n"
def test_next_tuple():
t = (SOURCE,)
i = iter(t)
n = next(i)
SINK(n) #$ MISSING: flow="SOURCE, l:-3 -> n"
def test_next_set():
s = {SOURCE}
i = iter(s)
n = next(i)
SINK(n) #$ MISSING: flow="SOURCE, l:-3 -> n"
def test_next_dict():
d = {SOURCE: "val"}
i = iter(d)
n = next(i)
SINK(n) #$ MISSING: flow="SOURCE, l:-3 -> n"

View File

@@ -25,13 +25,17 @@ uniqueParameterNodePosition
uniqueContentApprox uniqueContentApprox
identityLocalStep identityLocalStep
| test_async.py:48:9:48:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_async.py:48:9:48:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:56:10:56:21 | ControlFlowNode for tainted_list | Node steps to itself | | test_collections.py:64:10:64:21 | ControlFlowNode for tainted_list | Node steps to itself |
| test_collections.py:63:9:63:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:71:9:71:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:65:9:65:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:73:9:73:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:79:9:79:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:88:10:88:21 | ControlFlowNode for tainted_list | Node steps to itself |
| test_collections.py:81:9:81:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:89:10:89:23 | ControlFlowNode for TAINTED_STRING | Node steps to itself |
| test_collections.py:97:9:97:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:99:9:99:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:112:9:112:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:114:9:114:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:114:9:114:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:116:9:116:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_collections.py:147:9:147:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:213:9:213:15 | ControlFlowNode for my_dict | Node steps to itself | | test_collections.py:149:9:149:22 | ControlFlowNode for ensure_tainted | Node steps to itself |
| test_collections.py:213:22:213:33 | ControlFlowNode for tainted_dict | Node steps to itself | | test_collections.py:246:9:246:15 | ControlFlowNode for my_dict | Node steps to itself |
| test_collections.py:246:22:246:33 | ControlFlowNode for tainted_dict | Node steps to itself |
| test_for.py:24:9:24:22 | ControlFlowNode for ensure_tainted | Node steps to itself | | test_for.py:24:9:24:22 | ControlFlowNode for ensure_tainted | Node steps to itself |

View File

@@ -37,6 +37,14 @@ def test_construction():
tuple(tainted_list), # $ tainted tuple(tainted_list), # $ tainted
set(tainted_list), # $ tainted set(tainted_list), # $ tainted
frozenset(tainted_list), # $ tainted frozenset(tainted_list), # $ tainted
dict(tainted_dict), # $ tainted
dict(k = tainted_string)["k"], # $ MISSING: tainted
dict(dict(k = tainted_string))["k"], # $ MISSING: tainted
dict(["k", tainted_string]), # $ tainted
)
ensure_not_tainted(
dict(k = tainted_string)["k1"]
) )
@@ -64,6 +72,31 @@ def test_access(x, y, z):
for i in reversed(tainted_list): for i in reversed(tainted_list):
ensure_tainted(i) # $ tainted ensure_tainted(i) # $ tainted
def test_access_explicit(x, y, z):
tainted_list = [TAINTED_STRING]
ensure_tainted(
tainted_list[0], # $ tainted
tainted_list[x], # $ tainted
tainted_list[y:z], # $ tainted
sorted(tainted_list)[0], # $ tainted
reversed(tainted_list)[0], # $ tainted
iter(tainted_list), # $ tainted
next(iter(tainted_list)), # $ tainted
[i for i in tainted_list], # $ tainted
[tainted_list for i in [1,2,3]], # $ MISSING: tainted
[TAINTED_STRING for i in [1,2,3]], # $ tainted
[tainted_list], # $ tainted
)
a, b, c = tainted_list[0:3]
ensure_tainted(a, b, c) # $ tainted
for h in tainted_list:
ensure_tainted(h) # $ tainted
for i in reversed(tainted_list):
ensure_tainted(i) # $ tainted
def test_dict_access(x): def test_dict_access(x):
tainted_dict = TAINTED_DICT tainted_dict = TAINTED_DICT

View File

@@ -64,6 +64,7 @@ if __name__ == "__main__":
check_tests_valid("coverage.test") check_tests_valid("coverage.test")
check_tests_valid("coverage.argumentPassing") check_tests_valid("coverage.argumentPassing")
check_tests_valid("coverage.datamodel") check_tests_valid("coverage.datamodel")
check_tests_valid("coverage.test_builtins")
check_tests_valid("coverage-py2.classes") check_tests_valid("coverage-py2.classes")
check_tests_valid("coverage-py3.classes") check_tests_valid("coverage-py3.classes")
check_tests_valid("variable-capture.in") check_tests_valid("variable-capture.in")

View File

@@ -1,48 +1,21 @@
use clap::Args;
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use clap::Args;
use codeql_extractor::autobuilder;
#[derive(Args)] #[derive(Args)]
// The autobuilder takes no command-line options, but this may change in the future. // The autobuilder takes no command-line options, but this may change in the future.
pub struct Options {} pub struct Options {}
pub fn run(_: Options) -> std::io::Result<()> { pub fn run(_: Options) -> std::io::Result<()> {
let dist = env::var("CODEQL_DIST").expect("CODEQL_DIST not set"); let database = env::var("CODEQL_EXTRACTOR_QL_WIP_DATABASE")
let db = env::var("CODEQL_EXTRACTOR_QL_WIP_DATABASE")
.expect("CODEQL_EXTRACTOR_QL_WIP_DATABASE not set"); .expect("CODEQL_EXTRACTOR_QL_WIP_DATABASE not set");
let codeql = if env::consts::OS == "windows" {
"codeql.exe"
} else {
"codeql"
};
let codeql: PathBuf = [&dist, codeql].iter().collect();
let mut cmd = Command::new(codeql);
cmd.arg("database")
.arg("index-files")
.arg("--include-extension=.ql")
.arg("--include-extension=.qll")
.arg("--include-extension=.dbscheme")
.arg("--include-extension=.json")
.arg("--include-extension=.jsonc")
.arg("--include-extension=.jsonl")
.arg("--include=**/qlpack.yml")
.arg("--include=deprecated.blame")
.arg("--size-limit=10m")
.arg("--language=ql")
.arg("--working-dir=.")
.arg(db);
for line in env::var("LGTM_INDEX_FILTERS") autobuilder::Autobuilder::new("ql", PathBuf::from(database))
.unwrap_or_default() .include_extensions(&[".ql", ".qll", ".dbscheme", ".json", ".jsonc", ".jsonl"])
.split('\n') .include_globs(&["**/qlpack.yml", "deprecated.blame"])
{ .size_limit("10m")
if let Some(stripped) = line.strip_prefix("include:") { .run()
cmd.arg("--also-match=".to_owned() + stripped);
} else if let Some(stripped) = line.strip_prefix("exclude:") {
cmd.arg("--exclude=".to_owned() + stripped);
}
}
let exit = &cmd.spawn()?.wait()?;
std::process::exit(exit.code().unwrap_or(1))
} }

View File

@@ -25,13 +25,13 @@ class Location extends @location {
int getEndColumn() { locations_default(this, _, _, _, _, result) } int getEndColumn() { locations_default(this, _, _, _, _, result) }
/** Gets the number of lines covered by this location. */ /** Gets the number of lines covered by this location. */
int getNumLines() { result = getEndLine() - getStartLine() + 1 } int getNumLines() { result = this.getEndLine() - this.getStartLine() + 1 }
/** Gets a textual representation of this element. */ /** Gets a textual representation of this element. */
cached cached
string toString() { string toString() {
exists(string filepath, int startline, int startcolumn, int endline, int endcolumn | exists(string filepath, int startline, int startcolumn, int endline, int endcolumn |
hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) and this.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) and
result = filepath + "@" + startline + ":" + startcolumn + ":" + endline + ":" + endcolumn result = filepath + "@" + startline + ":" + startcolumn + ":" + endline + ":" + endcolumn
) )
} }

View File

@@ -16,7 +16,7 @@ class DisjunctionChain extends Disjunction {
Formula getOperand(int i) { Formula getOperand(int i) {
result = result =
rank[i + 1](Formula operand, Location l | rank[i + 1](Formula operand, Location l |
operand = getAnOperand*() and operand = this.getAnOperand*() and
not operand instanceof Disjunction and not operand instanceof Disjunction and
l = operand.getLocation() l = operand.getLocation()
| |
@@ -33,16 +33,16 @@ class DisjunctionChain extends Disjunction {
*/ */
class EqualsLiteral extends ComparisonFormula { class EqualsLiteral extends ComparisonFormula {
EqualsLiteral() { EqualsLiteral() {
getOperator() = "=" and this.getOperator() = "=" and
getAnOperand() instanceof Literal this.getAnOperand() instanceof Literal
} }
AstNode getOther() { AstNode getOther() {
result = getAnOperand() and result = this.getAnOperand() and
not result instanceof Literal not result instanceof Literal
} }
Literal getLiteral() { result = getAnOperand() } Literal getLiteral() { result = this.getAnOperand() }
} }
/** /**
@@ -60,29 +60,33 @@ class DisjunctionEqualsLiteral extends DisjunctionChain {
DisjunctionEqualsLiteral() { DisjunctionEqualsLiteral() {
// VarAccess on the same variable // VarAccess on the same variable
exists(VarDef v | exists(VarDef v |
forex(Formula f | f = getOperand(_) | forex(Formula f | f = this.getOperand(_) |
f.(EqualsLiteral).getAnOperand().(VarAccess).getDeclaration() = v f.(EqualsLiteral).getAnOperand().(VarAccess).getDeclaration() = v
) and ) and
firstOperand = getOperand(0).(EqualsLiteral).getAnOperand() and firstOperand = this.getOperand(0).(EqualsLiteral).getAnOperand() and
firstOperand.(VarAccess).getDeclaration() = v firstOperand.(VarAccess).getDeclaration() = v
) )
or or
// FieldAccess on the same variable // FieldAccess on the same variable
exists(FieldDecl v | exists(FieldDecl v |
forex(Formula f | f = getOperand(_) | forex(Formula f | f = this.getOperand(_) |
f.(EqualsLiteral).getAnOperand().(FieldAccess).getDeclaration() = v f.(EqualsLiteral).getAnOperand().(FieldAccess).getDeclaration() = v
) and ) and
firstOperand = getOperand(0).(EqualsLiteral).getAnOperand() and firstOperand = this.getOperand(0).(EqualsLiteral).getAnOperand() and
firstOperand.(FieldAccess).getDeclaration() = v firstOperand.(FieldAccess).getDeclaration() = v
) )
or or
// ThisAccess // ThisAccess
forex(Formula f | f = getOperand(_) | f.(EqualsLiteral).getAnOperand() instanceof ThisAccess) and forex(Formula f | f = this.getOperand(_) |
firstOperand = getOperand(0).(EqualsLiteral).getAnOperand().(ThisAccess) f.(EqualsLiteral).getAnOperand() instanceof ThisAccess
) and
firstOperand = this.getOperand(0).(EqualsLiteral).getAnOperand().(ThisAccess)
or or
// ResultAccess // ResultAccess
forex(Formula f | f = getOperand(_) | f.(EqualsLiteral).getAnOperand() instanceof ResultAccess) and forex(Formula f | f = this.getOperand(_) |
firstOperand = getOperand(0).(EqualsLiteral).getAnOperand().(ResultAccess) f.(EqualsLiteral).getAnOperand() instanceof ResultAccess
) and
firstOperand = this.getOperand(0).(EqualsLiteral).getAnOperand().(ResultAccess)
// (in principle something like GlobalValueNumbering could be used to generalize this) // (in principle something like GlobalValueNumbering could be used to generalize this)
} }
@@ -100,8 +104,8 @@ class DisjunctionEqualsLiteral extends DisjunctionChain {
*/ */
class CallLiteral extends Call { class CallLiteral extends Call {
CallLiteral() { CallLiteral() {
getNumberOfArguments() = 1 and this.getNumberOfArguments() = 1 and
getArgument(0) instanceof Literal this.getArgument(0) instanceof Literal
} }
} }
@@ -118,7 +122,7 @@ class DisjunctionPredicateLiteral extends DisjunctionChain {
DisjunctionPredicateLiteral() { DisjunctionPredicateLiteral() {
// Call to the same target // Call to the same target
exists(PredicateOrBuiltin target | exists(PredicateOrBuiltin target |
forex(Formula f | f = getOperand(_) | f.(CallLiteral).getTarget() = target) forex(Formula f | f = this.getOperand(_) | f.(CallLiteral).getTarget() = target)
) )
} }
} }

View File

@@ -8,3 +8,4 @@ extractor: ql
dependencies: dependencies:
codeql/typos: ${workspace} codeql/typos: ${workspace}
codeql/util: ${workspace} codeql/util: ${workspace}
warnOnImplicitThis: true

View File

@@ -7,7 +7,7 @@ query predicate test() { foo() }
class Foo extends AstNode { class Foo extends AstNode {
predicate bar() { none() } predicate bar() { none() }
predicate baz() { bar() } predicate baz() { this.bar() }
} }
class Sub extends Foo { class Sub extends Foo {

View File

@@ -5,7 +5,7 @@ getTarget
| Bar.qll:30:12:30:32 | MemberCall | Bar.qll:19:7:19:18 | ClassPredicate getParameter | | Bar.qll:30:12:30:32 | MemberCall | Bar.qll:19:7:19:18 | ClassPredicate getParameter |
| Baz.qll:8:18:8:44 | MemberCall | Baz.qll:4:10:4:24 | ClassPredicate getImportedPath | | Baz.qll:8:18:8:44 | MemberCall | Baz.qll:4:10:4:24 | ClassPredicate getImportedPath |
| Foo.qll:5:26:5:30 | PredicateCall | Foo.qll:3:11:3:13 | ClasslessPredicate foo | | Foo.qll:5:26:5:30 | PredicateCall | Foo.qll:3:11:3:13 | ClasslessPredicate foo |
| Foo.qll:10:21:10:25 | PredicateCall | Foo.qll:8:13:8:15 | ClassPredicate bar | | Foo.qll:10:21:10:30 | MemberCall | Foo.qll:8:13:8:15 | ClassPredicate bar |
| Foo.qll:14:34:14:44 | MemberCall | Foo.qll:10:13:10:15 | ClassPredicate baz | | Foo.qll:14:34:14:44 | MemberCall | Foo.qll:10:13:10:15 | ClassPredicate baz |
| Foo.qll:17:27:17:42 | MemberCall | Foo.qll:8:13:8:15 | ClassPredicate bar | | Foo.qll:17:27:17:42 | MemberCall | Foo.qll:8:13:8:15 | ClassPredicate bar |
| Foo.qll:29:5:29:16 | PredicateCall | Foo.qll:20:13:20:20 | ClasslessPredicate myThing2 | | Foo.qll:29:5:29:16 | PredicateCall | Foo.qll:20:13:20:20 | ClasslessPredicate myThing2 |

View File

@@ -1,45 +1,22 @@
use clap::Args;
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use clap::Args;
use codeql_extractor::autobuilder;
#[derive(Args)] #[derive(Args)]
// The autobuilder takes no command-line options, but this may change in the future. // The autobuilder takes no command-line options, but this may change in the future.
pub struct Options {} pub struct Options {}
pub fn run(_: Options) -> std::io::Result<()> { pub fn run(_: Options) -> std::io::Result<()> {
let dist = env::var("CODEQL_DIST").expect("CODEQL_DIST not set"); let database = env::var("CODEQL_EXTRACTOR_RUBY_WIP_DATABASE")
let db = env::var("CODEQL_EXTRACTOR_RUBY_WIP_DATABASE")
.expect("CODEQL_EXTRACTOR_RUBY_WIP_DATABASE not set"); .expect("CODEQL_EXTRACTOR_RUBY_WIP_DATABASE not set");
let codeql = if env::consts::OS == "windows" {
"codeql.exe"
} else {
"codeql"
};
let codeql: PathBuf = [&dist, codeql].iter().collect();
let mut cmd = Command::new(codeql);
cmd.arg("database")
.arg("index-files")
.arg("--include-extension=.rb")
.arg("--include-extension=.erb")
.arg("--include-extension=.gemspec")
.arg("--include=**/Gemfile")
.arg("--exclude=**/.git")
.arg("--size-limit=5m")
.arg("--language=ruby")
.arg("--working-dir=.")
.arg(db);
for line in env::var("LGTM_INDEX_FILTERS") autobuilder::Autobuilder::new("ruby", PathBuf::from(database))
.unwrap_or_default() .include_extensions(&[".rb", ".erb", ".gemspec"])
.split('\n') .include_globs(&["**/Gemfile"])
{ .exclude_globs(&["**/.git"])
if let Some(stripped) = line.strip_prefix("include:") { .size_limit("5m")
cmd.arg("--also-match=".to_owned() + stripped); .run()
} else if let Some(stripped) = line.strip_prefix("exclude:") {
cmd.arg("--exclude=".to_owned() + stripped);
}
}
let exit = &cmd.spawn()?.wait()?;
std::process::exit(exit.code().unwrap_or(1))
} }

View File

@@ -936,10 +936,10 @@ module ExprNodes {
} }
/** A control-flow node that wraps a `StringLiteral` AST expression. */ /** A control-flow node that wraps a `StringLiteral` AST expression. */
class StringLiteralCfgNode extends ExprCfgNode { class StringLiteralCfgNode extends StringlikeLiteralCfgNode {
override string getAPrimaryQlClass() { result = "StringLiteralCfgNode" } StringLiteralCfgNode() { e instanceof StringLiteral }
override StringLiteral e; override string getAPrimaryQlClass() { result = "StringLiteralCfgNode" }
final override StringLiteral getExpr() { result = super.getExpr() } final override StringLiteral getExpr() { result = super.getExpr() }
} }

View File

@@ -112,6 +112,13 @@ private module Cached {
) )
} }
cached
predicate summaryThroughStepTaint(
DataFlow::Node arg, DataFlow::Node out, FlowSummaryImpl::Public::SummarizedCallable sc
) {
FlowSummaryImpl::Private::Steps::summaryThroughStepTaint(arg, out, sc)
}
/** /**
* Holds if taint propagates from `nodeFrom` to `nodeTo` in exactly one local * Holds if taint propagates from `nodeFrom` to `nodeTo` in exactly one local
* (intra-procedural) step. * (intra-procedural) step.
@@ -122,7 +129,7 @@ private module Cached {
defaultAdditionalTaintStep(nodeFrom, nodeTo) or defaultAdditionalTaintStep(nodeFrom, nodeTo) or
// Simple flow through library code is included in the exposed local // Simple flow through library code is included in the exposed local
// step relation, even though flow is technically inter-procedural // step relation, even though flow is technically inter-procedural
FlowSummaryImpl::Private::Steps::summaryThroughStepTaint(nodeFrom, nodeTo, _) summaryThroughStepTaint(nodeFrom, nodeTo, _)
} }
} }

View File

@@ -21,6 +21,7 @@ private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.ApiGraphs private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts private import codeql.ruby.Concepts
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
private import codeql.ruby.dataflow.internal.TaintTrackingPrivate as TaintTrackingPrivate
private import codeql.ruby.TaintTracking private import codeql.ruby.TaintTracking
private import codeql.ruby.frameworks.core.String private import codeql.ruby.frameworks.core.String
@@ -37,43 +38,6 @@ DataFlow::LocalSourceNode strStart() {
/** Gets a dataflow node for a regular expression literal. */ /** Gets a dataflow node for a regular expression literal. */
DataFlow::LocalSourceNode regStart() { result.asExpr().getExpr() instanceof Ast::RegExpLiteral } DataFlow::LocalSourceNode regStart() { result.asExpr().getExpr() instanceof Ast::RegExpLiteral }
/**
* Holds if the analysis should track flow from `nodeFrom` to `nodeTo` on top of the ordinary type-tracking steps.
* `nodeFrom` and `nodeTo` has type `fromType` and `toType` respectively.
* The types are either "string" or "regexp".
*/
predicate step(
DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo, string fromType, string toType
) {
fromType = toType and
fromType = "string" and
(
// include taint flow through `String` summaries
TaintTracking::localTaintStep(nodeFrom, nodeTo) and
nodeFrom.(DataFlowPrivate::SummaryNode).getSummarizedCallable() instanceof
String::SummarizedCallable
or
// string concatenations, and
exists(CfgNodes::ExprNodes::OperationCfgNode op |
op = nodeTo.asExpr() and
op.getAnOperand() = nodeFrom.asExpr() and
op.getExpr().(Ast::BinaryOperation).getOperator() = "+"
)
or
// string interpolations
nodeFrom.asExpr() =
nodeTo.asExpr().(CfgNodes::ExprNodes::StringlikeLiteralCfgNode).getAComponent()
)
or
fromType = "string" and
toType = "reg" and
exists(DataFlow::CallNode call |
call = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]) and
nodeFrom = call.getArgument(0) and
nodeTo = call
)
}
/** Gets a node where string values that flow to the node are interpreted as regular expressions. */ /** Gets a node where string values that flow to the node are interpreted as regular expressions. */
DataFlow::Node stringSink() { DataFlow::Node stringSink() {
result instanceof RE::RegExpInterpretation::Range and result instanceof RE::RegExpInterpretation::Range and
@@ -91,28 +55,139 @@ DataFlow::Node stringSink() {
/** Gets a node where regular expressions that flow to the node are used. */ /** Gets a node where regular expressions that flow to the node are used. */
DataFlow::Node regSink() { result = any(RegexExecution exec).getRegex() } DataFlow::Node regSink() { result = any(RegexExecution exec).getRegex() }
/** Gets a node that is reachable by type-tracking from any string or regular expression. */ private signature module TypeTrackInputSig {
DataFlow::LocalSourceNode forward(TypeTracker t) { DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start);
t.start() and
result = [strStart(), regStart()] predicate end(DataFlow::Node n);
or
exists(TypeTracker t2 | result = forward(t2).track(t2, t)) predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo);
or
exists(TypeTracker t2 | t2 = t.continue() | step(forward(t2).getALocalUse(), result, _, _))
} }
/** /**
* Gets a node that is backwards reachable from any regular expression use, * Provides a version of type tracking where we first prune for reachable nodes,
* where that use is reachable by type-tracking from any string or regular expression. * before doing the type tracking computation.
*/ */
DataFlow::LocalSourceNode backwards(TypeBackTracker t) { private module TypeTrack<TypeTrackInputSig Input> {
private predicate additionalStep(
DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
) {
Input::additionalStep(nodeFrom.getALocalUse(), nodeTo)
}
/** Gets a node that is forwards reachable by type-tracking. */
pragma[nomagic]
private DataFlow::LocalSourceNode forward(TypeTracker t) {
result = Input::start(t, _)
or
exists(TypeTracker t2 | result = forward(t2).track(t2, t))
or
exists(TypeTracker t2 | t2 = t.continue() | additionalStep(forward(t2), result))
}
bindingset[result, tbt]
pragma[inline_late]
pragma[noopt]
private DataFlow::LocalSourceNode forwardLateInline(TypeBackTracker tbt) {
exists(TypeTracker tt |
result = forward(tt) and
tt = tbt.getACompatibleTypeTracker()
)
}
/** Gets a node that is backwards reachable by type-tracking. */
pragma[nomagic]
private DataFlow::LocalSourceNode backwards(TypeBackTracker t) {
result = forwardLateInline(t) and
(
t.start() and t.start() and
result.flowsTo([stringSink(), regSink()]) and Input::end(result.getALocalUse())
result = forward(TypeTracker::end())
or or
exists(TypeBackTracker t2 | result = backwards(t2).backtrack(t2, t)) exists(TypeBackTracker t2 | result = backwards(t2).backtrack(t2, t))
or or
exists(TypeBackTracker t2 | t2 = t.continue() | step(result.getALocalUse(), backwards(t2), _, _)) exists(TypeBackTracker t2 | t2 = t.continue() | additionalStep(result, backwards(t2)))
)
}
bindingset[result, tt]
pragma[inline_late]
pragma[noopt]
private DataFlow::LocalSourceNode backwardsInlineLate(TypeTracker tt) {
exists(TypeBackTracker tbt |
result = backwards(tbt) and
tt = tbt.getACompatibleTypeTracker()
)
}
/** Holds if `n` is forwards and backwards reachable with type tracker `t`. */
pragma[nomagic]
private predicate reached(DataFlow::LocalSourceNode n, TypeTracker t) {
n = forward(t) and
n = backwardsInlineLate(t)
}
pragma[nomagic]
private TypeTracker stepReached(
TypeTracker t, DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
) {
exists(StepSummary summary |
StepSummary::step(nodeFrom, nodeTo, summary) and
reached(nodeFrom, t) and
reached(nodeTo, result) and
result = t.append(summary)
)
or
additionalStep(nodeFrom, nodeTo) and
reached(nodeFrom, pragma[only_bind_into](t)) and
reached(nodeTo, pragma[only_bind_into](t)) and
result = t.continue()
}
/** Gets a node that has been tracked from the start node `start`. */
DataFlow::LocalSourceNode track(DataFlow::Node start, TypeTracker t) {
t.start() and
result = Input::start(t, start) and
reached(result, t)
or
exists(TypeTracker t2 | t = stepReached(t2, track(start, t2), result))
}
}
/** Holds if `inputStr` is compiled to a regular expression that is returned at `call`. */
pragma[nomagic]
private predicate regFromString(DataFlow::LocalSourceNode inputStr, DataFlow::CallNode call) {
exists(DataFlow::Node mid |
inputStr.flowsTo(mid) and
call = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]) and
mid = call.getArgument(0)
)
}
private module StringTypeTrackInput implements TypeTrackInputSig {
DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start) {
start = strStart() and t.start() and result = start
}
predicate end(DataFlow::Node n) {
n = stringSink() or
regFromString(n, _)
}
predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo) {
// include taint flow through `String` summaries
TaintTrackingPrivate::summaryThroughStepTaint(nodeFrom, nodeTo,
any(String::SummarizedCallable c))
or
// string concatenations, and
exists(CfgNodes::ExprNodes::OperationCfgNode op |
op = nodeTo.asExpr() and
op.getAnOperand() = nodeFrom.asExpr() and
op.getExpr().(Ast::BinaryOperation).getOperator() = "+"
)
or
// string interpolations
nodeFrom.asExpr() =
nodeTo.asExpr().(CfgNodes::ExprNodes::StringlikeLiteralCfgNode).getAComponent()
}
} }
/** /**
@@ -120,41 +195,34 @@ DataFlow::LocalSourceNode backwards(TypeBackTracker t) {
* This is used to figure out where `start` is evaluated as a regular expression against an input string, * This is used to figure out where `start` is evaluated as a regular expression against an input string,
* or where `start` is compiled into a regular expression. * or where `start` is compiled into a regular expression.
*/ */
private DataFlow::LocalSourceNode trackStrings(DataFlow::Node start, TypeTracker t) { private predicate trackStrings = TypeTrack<StringTypeTrackInput>::track/2;
result = backwards(_) and
( /** Holds if `strConst` flows to a regex compilation (tracked by `t`), where the resulting regular expression is stored in `reg`. */
pragma[nomagic]
private predicate regFromStringStart(DataFlow::Node strConst, TypeTracker t, DataFlow::CallNode reg) {
regFromString(trackStrings(strConst, t), reg) and
exists(t.continue())
}
private module RegTypeTrackInput implements TypeTrackInputSig {
DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start) {
start = regStart() and
t.start() and t.start() and
start = result and result = start
result = strStart()
or or
exists(TypeTracker t2 | result = trackStrings(start, t2).track(t2, t)) regFromStringStart(start, t, result)
or }
// an additional step from string to string
exists(TypeTracker t2 | t2 = t.continue() | predicate end(DataFlow::Node n) { n = regSink() }
step(trackStrings(start, t2).getALocalUse(), result, "string", "string")
) predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo) { none() }
)
} }
/** /**
* Gets a node that has been tracked from the regular expression `start` to some node. * Gets a node that has been tracked from the regular expression `start` to some node.
* This is used to figure out where `start` is executed against an input string. * This is used to figure out where `start` is executed against an input string.
*/ */
private DataFlow::LocalSourceNode trackRegs(DataFlow::Node start, TypeTracker t) { private predicate trackRegs = TypeTrack<RegTypeTrackInput>::track/2;
result = backwards(_) and
(
t.start() and
start = result and
result = regStart()
or
exists(TypeTracker t2 | result = trackRegs(start, t2).track(t2, t))
or
// an additional step where a string is converted to a regular expression
exists(TypeTracker t2 | t2 = t.continue() |
step(trackStrings(start, t2).getALocalUse(), result, "string", "reg")
)
)
}
/** Gets a node that references a regular expression. */ /** Gets a node that references a regular expression. */
private DataFlow::LocalSourceNode trackRegexpType(TypeTracker t) { private DataFlow::LocalSourceNode trackRegexpType(TypeTracker t) {

View File

@@ -613,8 +613,17 @@ class TypeBackTracker extends TTypeBackTracker {
* also flow to `sink`. * also flow to `sink`.
*/ */
TypeTracker getACompatibleTypeTracker() { TypeTracker getACompatibleTypeTracker() {
exists(boolean hasCall | result = MkTypeTracker(hasCall, content) | exists(boolean hasCall, OptionalTypeTrackerContent c |
hasCall = false or this.hasReturn() = false result = MkTypeTracker(hasCall, c) and
(
compatibleContents(c, content)
or
content = noContent() and c = content
)
|
hasCall = false
or
this.hasReturn() = false
) )
} }
} }

View File

@@ -3,3 +3,4 @@ version: 0.0.13-dev
groups: shared groups: shared
library: true library: true
dependencies: dependencies:
warnOnImplicitThis: true

View File

@@ -2,3 +2,4 @@ name: codeql/ssa
version: 0.0.17-dev version: 0.0.17-dev
groups: shared groups: shared
library: true library: true
warnOnImplicitThis: true

Some files were not shown because too many files have changed in this diff Show More