Skip to content

Commit 9e259b6

Browse files
authored
Merge pull request #7305 from github/nickrolfe/user-controlled-bypass
Ruby: query to find user-controlled bypass of sensitive actions
2 parents a7aff11 + 5765f36 commit 9e259b6

File tree

9 files changed

+346
-0
lines changed

9 files changed

+346
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Provides default sources, sinks, and sanitizers for reasoning about bypass of
3+
* sensitive action guards, as well as extension points for adding your own.
4+
*/
5+
6+
private import codeql.ruby.CFG
7+
private import codeql.ruby.DataFlow
8+
private import codeql.ruby.controlflow.BasicBlocks
9+
private import codeql.ruby.dataflow.RemoteFlowSources
10+
private import codeql.ruby.security.SensitiveActions
11+
12+
/**
13+
* Provides default sources, sinks, and sanitizers for reasoning about bypass of
14+
* sensitive action guards, as well as extension points for adding your own.
15+
*/
16+
module ConditionalBypass {
17+
/**
18+
* A data flow source for bypass of sensitive action guards.
19+
*/
20+
abstract class Source extends DataFlow::Node { }
21+
22+
/**
23+
* A data flow sink for bypass of sensitive action guards.
24+
*/
25+
abstract class Sink extends DataFlow::Node {
26+
/**
27+
* Gets the guarded sensitive action.
28+
*/
29+
abstract SensitiveAction getAction();
30+
}
31+
32+
/**
33+
* A sanitizer for bypass of sensitive action guards.
34+
*/
35+
abstract class Sanitizer extends DataFlow::Node { }
36+
37+
/**
38+
* A source of remote user input, considered as a flow source for bypass of
39+
* sensitive action guards.
40+
*/
41+
class RemoteFlowSourceAsSource extends Source {
42+
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
43+
}
44+
45+
/**
46+
* A conditional that guards a sensitive action, e.g. `ok` in `if (ok) login()`.
47+
*/
48+
class SensitiveActionGuardConditional extends Sink {
49+
SensitiveAction action;
50+
51+
SensitiveActionGuardConditional() {
52+
exists(ConditionBlock cb, BasicBlock controlled |
53+
cb.controls(controlled, _) and
54+
controlled.getANode() = action.asExpr() and
55+
cb.getLastNode() = this.asExpr()
56+
)
57+
}
58+
59+
override SensitiveAction getAction() { result = action }
60+
}
61+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about bypass of sensitive action guards.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `ConditionalBypass::Configuration` is needed, otherwise
6+
* `ConditionalBypassCustomizations` should be imported instead.
7+
*/
8+
9+
private import codeql.ruby.DataFlow
10+
private import codeql.ruby.TaintTracking
11+
private import codeql.ruby.security.SensitiveActions
12+
import ConditionalBypassCustomizations::ConditionalBypass
13+
14+
/**
15+
* A taint tracking configuration for bypass of sensitive action guards.
16+
*/
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "ConditionalBypass" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
23+
24+
override predicate isSanitizer(DataFlow::Node node) {
25+
super.isSanitizer(node) or
26+
node instanceof Sanitizer
27+
}
28+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Provides classes and predicates for identifying sensitive data and methods for security.
3+
*
4+
* 'Sensitive' data in general is anything that should not be sent around in unencrypted form. This
5+
* library tries to guess where sensitive data may either be stored in a variable or produced by a
6+
* method.
7+
*
8+
* In addition, there are methods that ought not to be executed or not in a fashion that the user
9+
* can control. This includes authorization methods such as logins, and sending of data, etc.
10+
*/
11+
12+
private import codeql.ruby.AST
13+
private import codeql.ruby.DataFlow
14+
15+
/**
16+
* A sensitive action, such as transfer of sensitive data.
17+
*/
18+
abstract class SensitiveAction extends DataFlow::Node { }
19+
20+
/** Holds if the return value from call `c` is ignored. */
21+
private predicate callWithIgnoredReturnValue(Call c) {
22+
exists(StmtSequence s, int i |
23+
(
24+
// If the call is a top-level statement within a statement sequence, its
25+
// return value (if any) is unused.
26+
c = s.getStmt(i)
27+
or
28+
// Or if the statement is an if-/unless-modifier expr and the call is its
29+
// branch.
30+
exists(ConditionalExpr cond |
31+
cond = s.getStmt(i) and
32+
c = cond.getBranch(_) and
33+
(cond instanceof IfModifierExpr or cond instanceof UnlessModifierExpr)
34+
)
35+
) and
36+
// But exclude calls that are the last statement, since they are evaluated
37+
// as the overall value of the sequence.
38+
exists(s.getStmt(i + 1))
39+
) and
40+
not c instanceof YieldCall and
41+
// Ignore statements in ERB output directives, which are evaluated.
42+
not exists(ErbOutputDirective d | d.getAChildStmt() = c)
43+
}
44+
45+
/** A call that may perform authorization. */
46+
class AuthorizationCall extends SensitiveAction, DataFlow::CallNode {
47+
AuthorizationCall() {
48+
exists(MethodCall c, string s |
49+
c = this.asExpr().getExpr() and
50+
s = c.getMethodName() // name contains `login` or `auth`, but not as part of `loginfo` or `unauth`;
51+
|
52+
// also exclude `author`
53+
s.regexpMatch("(?i).*(log_?in(?!fo)|(?<!un)auth(?!or\\b)|verify).*") and
54+
// but it does not start with `get` or `set`
55+
not s.regexpMatch("(?i)(get|set).*") and
56+
// Setter calls are unlikely to be sensitive actions.
57+
not c instanceof SetterMethodCall and
58+
(
59+
// Calls that have no return value (or ignore it) are likely to be
60+
// to methods that are actions.
61+
callWithIgnoredReturnValue(c)
62+
or
63+
// Method names ending in `!` are likely to be actions.
64+
s.matches("%!")
65+
)
66+
)
67+
}
68+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Using user-controlled data in a permissions check may allow a user to gain
8+
unauthorized access to protected functionality or data.
9+
</p>
10+
</overview>
11+
<recommendation>
12+
<p>
13+
When checking whether a user is authorized for a particular activity, do
14+
not use data that is entirely controlled by that user in the permissions
15+
check. If necessary, always validate the input, ideally against a fixed
16+
list of expected values.
17+
</p>
18+
<p>
19+
Similarly, do not decide which permission to check based on user data. In
20+
particular, avoid using computation to decide which permissions to check
21+
for. Use fixed permissions for particular actions, rather than generating
22+
the permission to check for.
23+
</p>
24+
</recommendation>
25+
<example>
26+
<p>
27+
In this example, the controller decided whether or not to authenticate the
28+
user based on the value of a request parameter.
29+
</p>
30+
<sample src="examples/bypass.rb" />
31+
</example>
32+
33+
<references>
34+
35+
</references>
36+
</qhelp>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @name User-controlled bypass of security check
3+
* @description Conditions controlled by the user are not suited for making security-related decisions.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.8
7+
* @precision medium
8+
* @id rb/user-controlled-bypass
9+
* @tags security
10+
* external/cwe/cwe-807
11+
* external/cwe/cwe-290
12+
*/
13+
14+
import ruby
15+
import codeql.ruby.DataFlow
16+
import codeql.ruby.dataflow.internal.DataFlowPublic
17+
import codeql.ruby.security.ConditionalBypassQuery
18+
import codeql.ruby.security.SensitiveActions
19+
import DataFlow::PathGraph
20+
21+
/**
22+
* Holds if the value of `nd` flows into `guard`.
23+
*/
24+
predicate flowsToGuardExpr(DataFlow::Node nd, SensitiveActionGuardConditional guard) {
25+
nd = guard
26+
or
27+
exists(DataFlow::Node succ | localFlowStep(nd, succ) | flowsToGuardExpr(succ, guard))
28+
}
29+
30+
/**
31+
* A comparison that guards a sensitive action, e.g. the comparison in:
32+
* ```rb
33+
* ok = x == y
34+
* if ok
35+
* login
36+
* end
37+
* ```
38+
*/
39+
class SensitiveActionGuardComparison extends ComparisonOperation {
40+
SensitiveActionGuardConditional guard;
41+
42+
SensitiveActionGuardComparison() {
43+
exists(DataFlow::Node node | this = node.asExpr().getExpr() | flowsToGuardExpr(node, guard))
44+
}
45+
46+
/**
47+
* Gets the guard that uses this comparison.
48+
*/
49+
SensitiveActionGuardConditional getGuard() { result = guard }
50+
}
51+
52+
/**
53+
* An intermediary sink to enable reuse of the taint configuration.
54+
* This sink should not be presented to the client of this query.
55+
*/
56+
class SensitiveActionGuardComparisonOperand extends Sink {
57+
SensitiveActionGuardComparison comparison;
58+
59+
SensitiveActionGuardComparisonOperand() { this.asExpr().getExpr() = comparison.getAnOperand() }
60+
61+
override SensitiveAction getAction() { result = comparison.getGuard().getAction() }
62+
}
63+
64+
/**
65+
* Holds if `sink` guards `action`, and `source` taints `sink`.
66+
*
67+
* If flow from `source` taints `sink`, then an attacker can
68+
* control if `action` should be executed or not.
69+
*/
70+
predicate isTaintedGuardForSensitiveAction(
71+
DataFlow::PathNode sink, DataFlow::PathNode source, SensitiveAction action
72+
) {
73+
action = sink.getNode().(Sink).getAction() and
74+
// exclude the intermediary sink
75+
not sink.getNode() instanceof SensitiveActionGuardComparisonOperand and
76+
exists(Configuration cfg | cfg.hasFlowPath(source, sink))
77+
}
78+
79+
from DataFlow::PathNode source, DataFlow::PathNode sink, SensitiveAction action
80+
where isTaintedGuardForSensitiveAction(sink, source, action)
81+
select sink.getNode(), source, sink, "This condition guards a sensitive $@, but $@ controls it.",
82+
action, "action", source.getNode(), "a user-provided value"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class UsersController < ActionController::Base
2+
def example
3+
user = User.find_by(login: params[:login])
4+
if params[:authenticate]
5+
# BAD: decision to take sensitive action based on user-controlled data
6+
log_in user
7+
redirect_to user
8+
end
9+
end
10+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
edges
2+
| ConditionalBypass.rb:3:13:3:18 | call to params : | ConditionalBypass.rb:6:8:6:12 | check |
3+
| ConditionalBypass.rb:14:14:14:19 | call to params : | ConditionalBypass.rb:14:14:14:27 | ...[...] |
4+
| ConditionalBypass.rb:25:10:25:15 | call to params : | ConditionalBypass.rb:25:10:25:22 | ...[...] |
5+
| ConditionalBypass.rb:25:10:25:15 | call to params : | ConditionalBypass.rb:25:10:25:22 | ...[...] : |
6+
| ConditionalBypass.rb:25:10:25:15 | call to params : | ConditionalBypass.rb:27:8:27:8 | p |
7+
| ConditionalBypass.rb:25:10:25:22 | ...[...] : | ConditionalBypass.rb:27:8:27:8 | p |
8+
nodes
9+
| ConditionalBypass.rb:3:13:3:18 | call to params : | semmle.label | call to params : |
10+
| ConditionalBypass.rb:6:8:6:12 | check | semmle.label | check |
11+
| ConditionalBypass.rb:14:14:14:19 | call to params : | semmle.label | call to params : |
12+
| ConditionalBypass.rb:14:14:14:27 | ...[...] | semmle.label | ...[...] |
13+
| ConditionalBypass.rb:25:10:25:15 | call to params : | semmle.label | call to params : |
14+
| ConditionalBypass.rb:25:10:25:22 | ...[...] | semmle.label | ...[...] |
15+
| ConditionalBypass.rb:25:10:25:22 | ...[...] : | semmle.label | ...[...] : |
16+
| ConditionalBypass.rb:27:8:27:8 | p | semmle.label | p |
17+
subpaths
18+
#select
19+
| ConditionalBypass.rb:6:8:6:12 | check | ConditionalBypass.rb:3:13:3:18 | call to params : | ConditionalBypass.rb:6:8:6:12 | check | This condition guards a sensitive $@, but $@ controls it. | ConditionalBypass.rb:8:7:8:29 | call to authenticate_user! | action | ConditionalBypass.rb:3:13:3:18 | call to params | a user-provided value |
20+
| ConditionalBypass.rb:14:14:14:27 | ...[...] | ConditionalBypass.rb:14:14:14:19 | call to params : | ConditionalBypass.rb:14:14:14:27 | ...[...] | This condition guards a sensitive $@, but $@ controls it. | ConditionalBypass.rb:14:5:14:9 | call to login | action | ConditionalBypass.rb:14:14:14:19 | call to params | a user-provided value |
21+
| ConditionalBypass.rb:27:8:27:8 | p | ConditionalBypass.rb:25:10:25:15 | call to params : | ConditionalBypass.rb:27:8:27:8 | p | This condition guards a sensitive $@, but $@ controls it. | ConditionalBypass.rb:28:7:28:13 | call to verify! | action | ConditionalBypass.rb:25:10:25:15 | call to params | a user-provided value |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/cwe-807/ConditionalBypass.ql
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class FooController < ActionController::Base
2+
def bad_handler1
3+
check = params[:check]
4+
name = params[:name]
5+
6+
if check
7+
# BAD
8+
authenticate_user! name
9+
end
10+
end
11+
12+
def bad_handler2
13+
# BAD
14+
login if params[:login]
15+
do_something_else
16+
end
17+
18+
def bad_handler3
19+
# BAD. Not detected: its the last statement in the method, so it doesn't
20+
# match the heuristic for an action.
21+
login if params[:login]
22+
end
23+
24+
def bad_handler4
25+
p = (params[:name] == "foo")
26+
# BAD
27+
if p
28+
verify!
29+
end
30+
end
31+
32+
def good_handler
33+
name = params[:name]
34+
# Call to a sensitive action, but the guard is not derived from user input.
35+
if should_auth_user?
36+
authenticate_user! name
37+
end
38+
end
39+
end

0 commit comments

Comments
 (0)