Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* String literals and arrays of string literals in case expression patterns are now recognised as barrier guards.
57 changes: 52 additions & 5 deletions ruby/ql/lib/codeql/ruby/controlflow/CfgNodes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ module ExprNodes {
override predicate relevantChild(AstNode n) { none() }
}

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

Expand Down Expand Up @@ -432,8 +432,36 @@ module ExprNodes {
final ExprCfgNode getBody() { e.hasCfgChild(e.getBody(), this, result) }
}

private class WhenClauseChildMapping extends NonExprChildMapping, WhenClause {
override predicate relevantChild(AstNode e) { e = [this.getBody(), this.getAPattern()] }
// `when` clauses need special treatment, since they are neither pre-order
// nor post-order
private class WhenClauseChildMapping extends WhenClause {
predicate patternReachesBasicBlock(int i, CfgNode cfnPattern, BasicBlock bb) {
exists(Expr pattern |
pattern = this.getPattern(i) and
cfnPattern.getNode() = pattern and
bb.getANode() = cfnPattern
)
or
exists(BasicBlock mid |
this.patternReachesBasicBlock(i, cfnPattern, mid) and
bb = mid.getASuccessor() and
not mid.getANode().getNode() = this
)
}

predicate bodyReachesBasicBlock(CfgNode cfnBody, BasicBlock bb) {
exists(Stmt body |
body = this.getBody() and
cfnBody.getNode() = body and
bb.getANode() = cfnBody
)
or
exists(BasicBlock mid |
this.bodyReachesBasicBlock(cfnBody, mid) and
bb = mid.getAPredecessor() and
not mid.getANode().getNode() = this
)
}
}

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

/** Gets the body of this `when`-clause. */
final ExprCfgNode getBody() { e.hasCfgChild(e.getBody(), this, result) }
final ExprCfgNode getBody() {
result.getNode() = desugar(e.getBody()) and
e.bodyReachesBasicBlock(result, this.getBasicBlock())
}

/** Gets the `i`th pattern this `when`-clause. */
final ExprCfgNode getPattern(int i) { e.hasCfgChild(e.getPattern(i), this, result) }
final ExprCfgNode getPattern(int i) {
result.getNode() = desugar(e.getPattern(i)) and
e.patternReachesBasicBlock(i, result, this.getBasicBlock())
}
}

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

private class SplatExprChildMapping extends OperationExprChildMapping, SplatExpr {
override predicate relevantChild(AstNode n) { n = this.getOperand() }
}

/** A control-flow node that wraps a `SplatExpr` AST expression. */
class SplatExprCfgNode extends UnaryOperationCfgNode {
override string getAPrimaryQlClass() { result = "SplatExprCfgNode" }

override SplatExprChildMapping e;

final override SplatExpr getExpr() { result = super.getExpr() }
}

/** A control-flow node that wraps an `ElementReference` AST expression. */
class ElementReferenceCfgNode extends MethodCallCfgNode {
override string getAPrimaryQlClass() { result = "ElementReferenceCfgNode" }
Expand Down
10 changes: 8 additions & 2 deletions ruby/ql/lib/codeql/ruby/controlflow/internal/Completion.qll
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,11 @@ private predicate inBooleanContext(AstNode n) {
or
exists(CaseExpr c, WhenClause w |
not exists(c.getValue()) and
c.getABranch() = w and
c.getABranch() = w
|
w.getPattern(_) = n
or
w = n
)
}

Expand All @@ -233,8 +236,11 @@ private predicate inMatchingContext(AstNode n) {
or
exists(CaseExpr c, WhenClause w |
exists(c.getValue()) and
c.getABranch() = w and
c.getABranch() = w
|
w.getPattern(_) = n
or
w = n
)
or
n instanceof CasePattern
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,9 @@ module Trees {
c instanceof SimpleCompletion
or
exists(int i, WhenTree branch | branch = this.getBranch(i) |
last(branch.getLastPattern(), pred, c) and
pred = branch and
first(this.getBranch(i + 1), succ) and
c.isValidFor(branch) and
c.(ConditionalCompletion).getValue() = false
)
or
Expand Down Expand Up @@ -1397,8 +1398,10 @@ module Trees {
final override ControlFlowTree getChildElement(int i) { result = this.getMethodName(i) }
}

private class WhenTree extends PreOrderTree, WhenClause {
final override predicate propagatesAbnormal(AstNode child) { child = this.getAPattern() }
private class WhenTree extends ControlFlowTree, WhenClause {
final override predicate propagatesAbnormal(AstNode child) {
child = [this.getAPattern(), this.getBody()]
}

final Expr getLastPattern() {
exists(int i |
Expand All @@ -1407,28 +1410,35 @@ module Trees {
)
}

final override predicate first(AstNode first) { first(this.getPattern(0), first) }

final override predicate last(AstNode last, Completion c) {
last(this.getLastPattern(), last, c) and
last = this and
c.isValidFor(this) and
c.(ConditionalCompletion).getValue() = false
or
last(this.getBody(), last, c)
}

final override predicate succ(AstNode pred, AstNode succ, Completion c) {
pred = this and
first(this.getPattern(0), succ) and
c instanceof SimpleCompletion
c.isValidFor(this) and
c.(ConditionalCompletion).getValue() = true and
first(this.getBody(), succ)
or
exists(int i, Expr p, boolean b |
p = this.getPattern(i) and
last(p, pred, c) and
b = c.(ConditionalCompletion).getValue()
|
b = true and
first(this.getBody(), succ)
succ = this
or
b = false and
first(this.getPattern(i + 1), succ)
or
not exists(this.getPattern(i + 1)) and
succ = this
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions ruby/ql/lib/codeql/ruby/controlflow/internal/Splitting.qll
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ private module ConditionalCompletionSplitting {
last(succ.(ConditionalExpr).getBranch(_), pred, c) and
completion = c
)
or
succ(pred, succ, c) and
succ instanceof WhenClause and
completion = c
}

override predicate hasEntryScope(CfgScope scope, AstNode succ) { none() }
Expand Down
115 changes: 107 additions & 8 deletions ruby/ql/lib/codeql/ruby/dataflow/BarrierGuards.qll
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,53 @@ private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.dataflow.SSA
private import codeql.ruby.ast.internal.Constant
private import codeql.ruby.InclusionTests
private import codeql.ruby.ast.internal.Literal

private predicate stringConstCompare(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
cached
private predicate stringConstCompare(CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch) {
exists(CfgNodes::ExprNodes::ComparisonOperationCfgNode c |
c = g and
c = guard and
exists(CfgNodes::ExprNodes::StringLiteralCfgNode strLitNode |
c.getExpr() instanceof EqExpr and branch = true
// Only consider strings without any interpolations
not strLitNode.getExpr().getComponent(_) instanceof StringInterpolationComponent and
c.getExpr() instanceof EqExpr and
branch = true
or
c.getExpr() instanceof CaseEqExpr and branch = true
or
c.getExpr() instanceof NEExpr and branch = false
|
c.getLeftOperand() = strLitNode and c.getRightOperand() = e
c.getLeftOperand() = strLitNode and c.getRightOperand() = testedNode
or
c.getLeftOperand() = e and c.getRightOperand() = strLitNode
c.getLeftOperand() = testedNode and c.getRightOperand() = strLitNode
)
)
or
stringConstCaseCompare(guard, testedNode, branch)
or
exists(CfgNodes::ExprNodes::BinaryOperationCfgNode g |
g = guard and
stringConstCompareOr(guard, branch) and
stringConstCompare(g.getLeftOperand(), testedNode, _)
)
}

/**
* Holds if `guard` is an `or` expression whose operands are string comparison guards.
* For example:
*
* ```rb
* x == "foo" or x == "bar"
* ```
*/
private predicate stringConstCompareOr(

Check warning

Code scanning / CodeQL

Missing QLDoc for parameter

The QLDoc has no documentation for branch, but the QLDoc mentions or
CfgNodes::ExprNodes::BinaryOperationCfgNode guard, boolean branch
) {
guard.getExpr() instanceof LogicalOrExpr and
branch = true and
forall(CfgNode innerGuard | innerGuard = guard.getAnOperand() |
stringConstCompare(innerGuard, any(Ssa::Definition def).getARead(), branch)
)
}

/**
Expand Down Expand Up @@ -72,10 +103,13 @@ deprecated class StringConstCompare extends DataFlow::BarrierGuard,
}
}

private predicate stringConstArrayInclusionCall(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
cached
private predicate stringConstArrayInclusionCall(
CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch
) {
exists(InclusionTest t |
t.asExpr() = g and
e = t.getContainedNode().asExpr() and
t.asExpr() = guard and
testedNode = t.getContainedNode().asExpr() and
branch = t.getPolarity()
|
exists(ExprNodes::ArrayLiteralCfgNode arr |
Expand Down Expand Up @@ -132,3 +166,68 @@ deprecated class StringConstArrayInclusionCall extends DataFlow::BarrierGuard,

override predicate checks(CfgNode expr, boolean branch) { expr = checkedNode and branch = true }
}

/**
* A validation of a value by comparing with a constant string via a `case`
* expression. For example:
*
* ```rb
* name = params[:user_name]
* case name
* when "alice"
* User.find_by("username = #{name}")
* when *["bob", "charlie"]
* User.find_by("username = #{name}")
* when "dave", "eve" # this is not yet recognised as a barrier guard
* User.find_by("username = #{name}")
* end
* ```
*/
private predicate stringConstCaseCompare(
CfgNodes::AstCfgNode guard, CfgNode testedNode, boolean branch
) {
branch = true and
exists(CfgNodes::ExprNodes::CaseExprCfgNode case |
case.getValue() = testedNode and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also handle the case where getValue does not exist?

case
  when foo == "foo" then ...
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, I didn't know you could do this. This appears to already be covered by our existing StringConstCompareBarrier. I'll add a test for it.

(
guard =
any(CfgNodes::ExprNodes::WhenClauseCfgNode branchNode |
branchNode = case.getBranch(_) and
// For simplicity, consider patterns that contain only string literals or arrays of string literals
forall(ExprCfgNode pattern | pattern = branchNode.getPattern(_) |
// when "foo"
// when "foo", "bar"
pattern instanceof ExprNodes::StringLiteralCfgNode
or
pattern =
any(CfgNodes::ExprNodes::SplatExprCfgNode splat |
// when *["foo", "bar"]
forex(ExprCfgNode elem |
elem = splat.getOperand().(ExprNodes::ArrayLiteralCfgNode).getAnArgument()
|
elem instanceof ExprNodes::StringLiteralCfgNode
)
or
// when *some_var
// when *SOME_CONST
exists(ExprNodes::ArrayLiteralCfgNode arr |
isArrayConstant(splat.getOperand(), arr) and
forall(ExprCfgNode elem | elem = arr.getAnArgument() |
elem instanceof ExprNodes::StringLiteralCfgNode
)
)
)
)
)
or
// in "foo"
exists(
CfgNodes::ExprNodes::InClauseCfgNode branchNode, ExprNodes::StringLiteralCfgNode pattern
|
branchNode = case.getBranch(_) and
pattern = branchNode.getPattern() and
guard = pattern
)
)
)
}
12 changes: 6 additions & 6 deletions ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPublic.qll
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ class ContentSet extends TContentSet {
* For example, the guard `g` might be a call `isSafe(x)` and the expression `e`
* the argument `x`.
*/
signature predicate guardChecksSig(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch);
signature predicate guardChecksSig(CfgNodes::AstCfgNode g, CfgNode e, boolean branch);

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

pragma[nomagic]
private predicate guardControlsSsaDef(
CfgNodes::ExprCfgNode g, boolean branch, Ssa::Definition def, Node n
CfgNodes::AstCfgNode g, boolean branch, Ssa::Definition def, Node n
) {
def.getARead() = n.asExpr() and
guardControlsBlock(g, n.asExpr().getBasicBlock(), branch)
}

/** Gets a node that is safely guarded by the given guard check. */
Node getABarrierNode() {
exists(CfgNodes::ExprCfgNode g, boolean branch, Ssa::Definition def |
exists(CfgNodes::AstCfgNode g, boolean branch, Ssa::Definition def |
guardChecksSsaDef(g, branch, def) and
guardControlsSsaDef(g, branch, def, result)
)
Expand Down Expand Up @@ -488,8 +488,8 @@ module BarrierGuard<guardChecksSig/3 guardChecks> {
}

/** Holds if the guard `guard` controls block `bb` upon evaluating to `branch`. */
private predicate guardControlsBlock(CfgNodes::ExprCfgNode guard, BasicBlock bb, boolean branch) {
exists(ConditionBlock conditionBlock, SuccessorTypes::BooleanSuccessor s |
private predicate guardControlsBlock(CfgNodes::AstCfgNode guard, BasicBlock bb, boolean branch) {
exists(ConditionBlock conditionBlock, SuccessorTypes::ConditionalSuccessor s |
guard = conditionBlock.getLastNode() and
s.getValue() = branch and
conditionBlock.controls(bb, s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ module PolynomialReDoS {
override DataFlow::Node getHighlight() { result = matchNode }
}

private predicate lengthGuard(CfgNodes::ExprCfgNode g, CfgNode node, boolean branch) {
private predicate lengthGuard(CfgNodes::AstCfgNode g, CfgNode node, boolean branch) {
exists(DataFlow::Node input, DataFlow::CallNode length, DataFlow::ExprNode operand |
length.asExpr().getExpr().(Ast::MethodCall).getMethodName() = "length" and
length.getReceiver() = input and
Expand Down
Loading