Skip to content

Commit 375403f

Browse files
authored
Merge pull request github#11114 from hmac/case-barrier-guard-3
Ruby: Add case string comparison barrier guard
2 parents 2241252 + 2822c94 commit 375403f

File tree

14 files changed

+714
-92
lines changed

14 files changed

+714
-92
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* String literals and arrays of string literals in case expression patterns are now recognised as barrier guards.

ruby/ql/lib/codeql/ruby/controlflow/CfgNodes.qll

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ module ExprNodes {
234234
override predicate relevantChild(AstNode n) { none() }
235235
}
236236

237-
/** A control-flow node that wraps an `ArrayLiteral` AST expression. */
237+
/** A control-flow node that wraps a `Literal` AST expression. */
238238
class LiteralCfgNode extends ExprCfgNode {
239239
override string getAPrimaryQlClass() { result = "LiteralCfgNode" }
240240

@@ -432,8 +432,36 @@ module ExprNodes {
432432
final ExprCfgNode getBody() { e.hasCfgChild(e.getBody(), this, result) }
433433
}
434434

435-
private class WhenClauseChildMapping extends NonExprChildMapping, WhenClause {
436-
override predicate relevantChild(AstNode e) { e = [this.getBody(), this.getAPattern()] }
435+
// `when` clauses need special treatment, since they are neither pre-order
436+
// nor post-order
437+
private class WhenClauseChildMapping extends WhenClause {
438+
predicate patternReachesBasicBlock(int i, CfgNode cfnPattern, BasicBlock bb) {
439+
exists(Expr pattern |
440+
pattern = this.getPattern(i) and
441+
cfnPattern.getNode() = pattern and
442+
bb.getANode() = cfnPattern
443+
)
444+
or
445+
exists(BasicBlock mid |
446+
this.patternReachesBasicBlock(i, cfnPattern, mid) and
447+
bb = mid.getASuccessor() and
448+
not mid.getANode().getNode() = this
449+
)
450+
}
451+
452+
predicate bodyReachesBasicBlock(CfgNode cfnBody, BasicBlock bb) {
453+
exists(Stmt body |
454+
body = this.getBody() and
455+
cfnBody.getNode() = body and
456+
bb.getANode() = cfnBody
457+
)
458+
or
459+
exists(BasicBlock mid |
460+
this.bodyReachesBasicBlock(cfnBody, mid) and
461+
bb = mid.getAPredecessor() and
462+
not mid.getANode().getNode() = this
463+
)
464+
}
437465
}
438466

439467
/** A control-flow node that wraps a `WhenClause` AST expression. */
@@ -443,10 +471,16 @@ module ExprNodes {
443471
override WhenClauseChildMapping e;
444472

445473
/** Gets the body of this `when`-clause. */
446-
final ExprCfgNode getBody() { e.hasCfgChild(e.getBody(), this, result) }
474+
final ExprCfgNode getBody() {
475+
result.getNode() = desugar(e.getBody()) and
476+
e.bodyReachesBasicBlock(result, this.getBasicBlock())
477+
}
447478

448479
/** Gets the `i`th pattern this `when`-clause. */
449-
final ExprCfgNode getPattern(int i) { e.hasCfgChild(e.getPattern(i), this, result) }
480+
final ExprCfgNode getPattern(int i) {
481+
result.getNode() = desugar(e.getPattern(i)) and
482+
e.patternReachesBasicBlock(i, result, this.getBasicBlock())
483+
}
450484
}
451485

452486
/** A control-flow node that wraps a `CasePattern`. */
@@ -866,6 +900,19 @@ module ExprNodes {
866900
final override RelationalOperation getExpr() { result = super.getExpr() }
867901
}
868902

903+
private class SplatExprChildMapping extends OperationExprChildMapping, SplatExpr {
904+
override predicate relevantChild(AstNode n) { n = this.getOperand() }
905+
}
906+
907+
/** A control-flow node that wraps a `SplatExpr` AST expression. */
908+
class SplatExprCfgNode extends UnaryOperationCfgNode {
909+
override string getAPrimaryQlClass() { result = "SplatExprCfgNode" }
910+
911+
override SplatExprChildMapping e;
912+
913+
final override SplatExpr getExpr() { result = super.getExpr() }
914+
}
915+
869916
/** A control-flow node that wraps an `ElementReference` AST expression. */
870917
class ElementReferenceCfgNode extends MethodCallCfgNode {
871918
override string getAPrimaryQlClass() { result = "ElementReferenceCfgNode" }

ruby/ql/lib/codeql/ruby/controlflow/internal/Completion.qll

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,11 @@ private predicate inBooleanContext(AstNode n) {
211211
or
212212
exists(CaseExpr c, WhenClause w |
213213
not exists(c.getValue()) and
214-
c.getABranch() = w and
214+
c.getABranch() = w
215+
|
215216
w.getPattern(_) = n
217+
or
218+
w = n
216219
)
217220
}
218221

@@ -233,8 +236,11 @@ private predicate inMatchingContext(AstNode n) {
233236
or
234237
exists(CaseExpr c, WhenClause w |
235238
exists(c.getValue()) and
236-
c.getABranch() = w and
239+
c.getABranch() = w
240+
|
237241
w.getPattern(_) = n
242+
or
243+
w = n
238244
)
239245
or
240246
n instanceof CasePattern

ruby/ql/lib/codeql/ruby/controlflow/internal/ControlFlowGraphImpl.qll

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,9 @@ module Trees {
400400
c instanceof SimpleCompletion
401401
or
402402
exists(int i, WhenTree branch | branch = this.getBranch(i) |
403-
last(branch.getLastPattern(), pred, c) and
403+
pred = branch and
404404
first(this.getBranch(i + 1), succ) and
405+
c.isValidFor(branch) and
405406
c.(ConditionalCompletion).getValue() = false
406407
)
407408
or
@@ -1397,8 +1398,10 @@ module Trees {
13971398
final override ControlFlowTree getChildElement(int i) { result = this.getMethodName(i) }
13981399
}
13991400

1400-
private class WhenTree extends PreOrderTree, WhenClause {
1401-
final override predicate propagatesAbnormal(AstNode child) { child = this.getAPattern() }
1401+
private class WhenTree extends ControlFlowTree, WhenClause {
1402+
final override predicate propagatesAbnormal(AstNode child) {
1403+
child = [this.getAPattern(), this.getBody()]
1404+
}
14021405

14031406
final Expr getLastPattern() {
14041407
exists(int i |
@@ -1407,28 +1410,35 @@ module Trees {
14071410
)
14081411
}
14091412

1413+
final override predicate first(AstNode first) { first(this.getPattern(0), first) }
1414+
14101415
final override predicate last(AstNode last, Completion c) {
1411-
last(this.getLastPattern(), last, c) and
1416+
last = this and
1417+
c.isValidFor(this) and
14121418
c.(ConditionalCompletion).getValue() = false
14131419
or
14141420
last(this.getBody(), last, c)
14151421
}
14161422

14171423
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
14181424
pred = this and
1419-
first(this.getPattern(0), succ) and
1420-
c instanceof SimpleCompletion
1425+
c.isValidFor(this) and
1426+
c.(ConditionalCompletion).getValue() = true and
1427+
first(this.getBody(), succ)
14211428
or
14221429
exists(int i, Expr p, boolean b |
14231430
p = this.getPattern(i) and
14241431
last(p, pred, c) and
14251432
b = c.(ConditionalCompletion).getValue()
14261433
|
14271434
b = true and
1428-
first(this.getBody(), succ)
1435+
succ = this
14291436
or
14301437
b = false and
14311438
first(this.getPattern(i + 1), succ)
1439+
or
1440+
not exists(this.getPattern(i + 1)) and
1441+
succ = this
14321442
)
14331443
}
14341444
}

ruby/ql/lib/codeql/ruby/controlflow/internal/Splitting.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ private module ConditionalCompletionSplitting {
8686
last(succ.(ConditionalExpr).getBranch(_), pred, c) and
8787
completion = c
8888
)
89+
or
90+
succ(pred, succ, c) and
91+
succ instanceof WhenClause and
92+
completion = c
8993
}
9094

9195
override predicate hasEntryScope(CfgScope scope, AstNode succ) { none() }

ruby/ql/lib/codeql/ruby/dataflow/BarrierGuards.qll

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,53 @@ private import codeql.ruby.controlflow.CfgNodes
77
private import codeql.ruby.dataflow.SSA
88
private import codeql.ruby.ast.internal.Constant
99
private import codeql.ruby.InclusionTests
10+
private import codeql.ruby.ast.internal.Literal
1011

11-
private predicate stringConstCompare(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
12+
cached
13+
private predicate stringConstCompare(CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch) {
1214
exists(CfgNodes::ExprNodes::ComparisonOperationCfgNode c |
13-
c = g and
15+
c = guard and
1416
exists(CfgNodes::ExprNodes::StringLiteralCfgNode strLitNode |
15-
c.getExpr() instanceof EqExpr and branch = true
17+
// Only consider strings without any interpolations
18+
not strLitNode.getExpr().getComponent(_) instanceof StringInterpolationComponent and
19+
c.getExpr() instanceof EqExpr and
20+
branch = true
1621
or
1722
c.getExpr() instanceof CaseEqExpr and branch = true
1823
or
1924
c.getExpr() instanceof NEExpr and branch = false
2025
|
21-
c.getLeftOperand() = strLitNode and c.getRightOperand() = e
26+
c.getLeftOperand() = strLitNode and c.getRightOperand() = testedNode
2227
or
23-
c.getLeftOperand() = e and c.getRightOperand() = strLitNode
28+
c.getLeftOperand() = testedNode and c.getRightOperand() = strLitNode
2429
)
2530
)
31+
or
32+
stringConstCaseCompare(guard, testedNode, branch)
33+
or
34+
exists(CfgNodes::ExprNodes::BinaryOperationCfgNode g |
35+
g = guard and
36+
stringConstCompareOr(guard, branch) and
37+
stringConstCompare(g.getLeftOperand(), testedNode, _)
38+
)
39+
}
40+
41+
/**
42+
* Holds if `guard` is an `or` expression whose operands are string comparison guards.
43+
* For example:
44+
*
45+
* ```rb
46+
* x == "foo" or x == "bar"
47+
* ```
48+
*/
49+
private predicate stringConstCompareOr(
50+
CfgNodes::ExprNodes::BinaryOperationCfgNode guard, boolean branch
51+
) {
52+
guard.getExpr() instanceof LogicalOrExpr and
53+
branch = true and
54+
forall(CfgNode innerGuard | innerGuard = guard.getAnOperand() |
55+
stringConstCompare(innerGuard, any(Ssa::Definition def).getARead(), branch)
56+
)
2657
}
2758

2859
/**
@@ -72,10 +103,13 @@ deprecated class StringConstCompare extends DataFlow::BarrierGuard,
72103
}
73104
}
74105

75-
private predicate stringConstArrayInclusionCall(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
106+
cached
107+
private predicate stringConstArrayInclusionCall(
108+
CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch
109+
) {
76110
exists(InclusionTest t |
77-
t.asExpr() = g and
78-
e = t.getContainedNode().asExpr() and
111+
t.asExpr() = guard and
112+
testedNode = t.getContainedNode().asExpr() and
79113
branch = t.getPolarity()
80114
|
81115
exists(ExprNodes::ArrayLiteralCfgNode arr |
@@ -132,3 +166,68 @@ deprecated class StringConstArrayInclusionCall extends DataFlow::BarrierGuard,
132166

133167
override predicate checks(CfgNode expr, boolean branch) { expr = checkedNode and branch = true }
134168
}
169+
170+
/**
171+
* A validation of a value by comparing with a constant string via a `case`
172+
* expression. For example:
173+
*
174+
* ```rb
175+
* name = params[:user_name]
176+
* case name
177+
* when "alice"
178+
* User.find_by("username = #{name}")
179+
* when *["bob", "charlie"]
180+
* User.find_by("username = #{name}")
181+
* when "dave", "eve" # this is not yet recognised as a barrier guard
182+
* User.find_by("username = #{name}")
183+
* end
184+
* ```
185+
*/
186+
private predicate stringConstCaseCompare(
187+
CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch
188+
) {
189+
branch = true and
190+
exists(CfgNodes::ExprNodes::CaseExprCfgNode case |
191+
case.getValue() = testedNode and
192+
(
193+
guard =
194+
any(CfgNodes::ExprNodes::WhenClauseCfgNode branchNode |
195+
branchNode = case.getBranch(_) and
196+
// For simplicity, consider patterns that contain only string literals or arrays of string literals
197+
forall(ExprCfgNode pattern | pattern = branchNode.getPattern(_) |
198+
// when "foo"
199+
// when "foo", "bar"
200+
pattern instanceof ExprNodes::StringLiteralCfgNode
201+
or
202+
pattern =
203+
any(CfgNodes::ExprNodes::SplatExprCfgNode splat |
204+
// when *["foo", "bar"]
205+
forex(ExprCfgNode elem |
206+
elem = splat.getOperand().(ExprNodes::ArrayLiteralCfgNode).getAnArgument()
207+
|
208+
elem instanceof ExprNodes::StringLiteralCfgNode
209+
)
210+
or
211+
// when *some_var
212+
// when *SOME_CONST
213+
exists(ExprNodes::ArrayLiteralCfgNode arr |
214+
isArrayConstant(splat.getOperand(), arr) and
215+
forall(ExprCfgNode elem | elem = arr.getAnArgument() |
216+
elem instanceof ExprNodes::StringLiteralCfgNode
217+
)
218+
)
219+
)
220+
)
221+
)
222+
or
223+
// in "foo"
224+
exists(
225+
CfgNodes::ExprNodes::InClauseCfgNode branchNode, ExprNodes::StringLiteralCfgNode pattern
226+
|
227+
branchNode = case.getBranch(_) and
228+
pattern = branchNode.getPattern() and
229+
guard = pattern
230+
)
231+
)
232+
)
233+
}

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPublic.qll

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ class ContentSet extends TContentSet {
615615
* For example, the guard `g` might be a call `isSafe(x)` and the expression `e`
616616
* the argument `x`.
617617
*/
618-
signature predicate guardChecksSig(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch);
618+
signature predicate guardChecksSig(CfgNodes::AstCfgNode g, CfgNode e, boolean branch);
619619

620620
/**
621621
* Provides a set of barrier nodes for a guard that validates an expression.
@@ -625,21 +625,21 @@ signature predicate guardChecksSig(CfgNodes::ExprCfgNode g, CfgNode e, boolean b
625625
*/
626626
module BarrierGuard<guardChecksSig/3 guardChecks> {
627627
pragma[nomagic]
628-
private predicate guardChecksSsaDef(CfgNodes::ExprCfgNode g, boolean branch, Ssa::Definition def) {
628+
private predicate guardChecksSsaDef(CfgNodes::AstCfgNode g, boolean branch, Ssa::Definition def) {
629629
guardChecks(g, def.getARead(), branch)
630630
}
631631

632632
pragma[nomagic]
633633
private predicate guardControlsSsaDef(
634-
CfgNodes::ExprCfgNode g, boolean branch, Ssa::Definition def, Node n
634+
CfgNodes::AstCfgNode g, boolean branch, Ssa::Definition def, Node n
635635
) {
636636
def.getARead() = n.asExpr() and
637637
guardControlsBlock(g, n.asExpr().getBasicBlock(), branch)
638638
}
639639

640640
/** Gets a node that is safely guarded by the given guard check. */
641641
Node getABarrierNode() {
642-
exists(CfgNodes::ExprCfgNode g, boolean branch, Ssa::Definition def |
642+
exists(CfgNodes::AstCfgNode g, boolean branch, Ssa::Definition def |
643643
guardChecksSsaDef(g, branch, def) and
644644
guardControlsSsaDef(g, branch, def, result)
645645
)
@@ -669,8 +669,8 @@ module BarrierGuard<guardChecksSig/3 guardChecks> {
669669
}
670670

671671
/** Holds if the guard `guard` controls block `bb` upon evaluating to `branch`. */
672-
private predicate guardControlsBlock(CfgNodes::ExprCfgNode guard, BasicBlock bb, boolean branch) {
673-
exists(ConditionBlock conditionBlock, SuccessorTypes::BooleanSuccessor s |
672+
private predicate guardControlsBlock(CfgNodes::AstCfgNode guard, BasicBlock bb, boolean branch) {
673+
exists(ConditionBlock conditionBlock, SuccessorTypes::ConditionalSuccessor s |
674674
guard = conditionBlock.getLastNode() and
675675
s.getValue() = branch and
676676
conditionBlock.controls(bb, s)

ruby/ql/lib/codeql/ruby/security/regexp/PolynomialReDoSCustomizations.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ module PolynomialReDoS {
129129
override DataFlow::Node getHighlight() { result = matchNode }
130130
}
131131

132-
private predicate lengthGuard(CfgNodes::ExprCfgNode g, CfgNode node, boolean branch) {
132+
private predicate lengthGuard(CfgNodes::AstCfgNode g, CfgNode node, boolean branch) {
133133
exists(DataFlow::Node input, DataFlow::CallNode length, DataFlow::ExprNode operand |
134134
length.asExpr().getExpr().(Ast::MethodCall).getMethodName() = "length" and
135135
length.getReceiver() = input and

0 commit comments

Comments
 (0)