Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dbf1805

Browse files
authored
Merge pull request #196 from github/active-record-1
Start modelling some potential SQL fragment sinks in ActiveRecord
2 parents bedd790 + 7439ab5 commit dbf1805

5 files changed

Lines changed: 247 additions & 0 deletions

File tree

ql/src/codeql_ruby/Concepts.qll

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Provides abstract classes representing generic concepts such as file system
3+
* access or system command execution, for which individual framework libraries
4+
* provide concrete subclasses.
5+
*/
6+
7+
private import codeql_ruby.DataFlow
8+
9+
/**
10+
* A data-flow node that executes SQL statements.
11+
*
12+
* Extend this class to refine existing API models. If you want to model new APIs,
13+
* extend `SqlExecution::Range` instead.
14+
*/
15+
class SqlExecution extends DataFlow::Node {
16+
SqlExecution::Range range;
17+
18+
SqlExecution() { this = range }
19+
20+
/** Gets the argument that specifies the SQL statements to be executed. */
21+
DataFlow::Node getSql() { result = range.getSql() }
22+
}
23+
24+
/** Provides a class for modeling new SQL execution APIs. */
25+
module SqlExecution {
26+
/**
27+
* A data-flow node that executes SQL statements.
28+
*
29+
* Extend this class to model new APIs. If you want to refine existing API models,
30+
* extend `SqlExecution` instead.
31+
*/
32+
abstract class Range extends DataFlow::Node {
33+
/** Gets the argument that specifies the SQL statements to be executed. */
34+
abstract DataFlow::Node getSql();
35+
}
36+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
private import codeql_ruby.AST
2+
private import codeql_ruby.Concepts
3+
private import codeql_ruby.controlflow.CfgNodes
4+
private import codeql_ruby.DataFlow
5+
private import codeql_ruby.ast.internal.Module
6+
7+
private class ActiveRecordBaseAccess extends ConstantReadAccess {
8+
ActiveRecordBaseAccess() {
9+
this.getName() = "Base" and
10+
this.getScopeExpr().(ConstantAccess).getName() = "ActiveRecord"
11+
}
12+
}
13+
14+
// ApplicationRecord extends ActiveRecord::Base, but we
15+
// treat it separately in case the ApplicationRecord definition
16+
// is not in the database
17+
private class ApplicationRecordAccess extends ConstantReadAccess {
18+
ApplicationRecordAccess() { this.getName() = "ApplicationRecord" }
19+
}
20+
21+
/**
22+
* A `ClassDeclaration` for a class that extends `ActiveRecord::Base`. For example,
23+
*
24+
* ```rb
25+
* class UserGroup < ActiveRecord::Base
26+
* has_many :users
27+
* end
28+
* ```
29+
*/
30+
class ActiveRecordModelClass extends ClassDeclaration {
31+
ActiveRecordModelClass() {
32+
// class Foo < ActiveRecord::Base
33+
this.getSuperclassExpr() instanceof ActiveRecordBaseAccess
34+
or
35+
// class Foo < ApplicationRecord
36+
this.getSuperclassExpr() instanceof ApplicationRecordAccess
37+
or
38+
// class Bar < Foo
39+
exists(ActiveRecordModelClass other |
40+
other.getModule() = resolveScopeExpr(this.getSuperclassExpr())
41+
)
42+
}
43+
}
44+
45+
/** A class method call whose receiver is an `ActiveRecordModelClass`. */
46+
class ActiveRecordModelClassMethodCall extends MethodCall {
47+
ActiveRecordModelClassMethodCall() {
48+
// e.g. Foo.where(...)
49+
exists(ActiveRecordModelClass recvCls |
50+
recvCls.getModule() = resolveScopeExpr(this.getReceiver())
51+
)
52+
or
53+
// e.g. Foo.joins(:bars).where(...)
54+
this.getReceiver() instanceof ActiveRecordModelClassMethodCall
55+
}
56+
}
57+
58+
private predicate methodWithSqlFragmentArg(string methodName, int argIndex) {
59+
methodName =
60+
[
61+
"delete_all", "destroy_all", "exists?", "find_by", "find_by_sql", "from", "group", "having",
62+
"joins", "lock", "not", "order", "pluck", "where"
63+
] and
64+
argIndex = 0
65+
or
66+
methodName = "calculate" and argIndex = 1
67+
}
68+
69+
/**
70+
* A method call that may result in executing unintended user-controlled SQL
71+
* queries if the `getSqlFragmentSinkArgument()` expression is tainted by
72+
* unsanitized user-controlled input. For example, supposing that `User` is an
73+
* `ActiveRecord` model class, then
74+
*
75+
* ```rb
76+
* User.where("name = '#{user_name}'")
77+
* ```
78+
*
79+
* may be unsafe if `user_name` is from unsanitized user input, as a value such
80+
* as `"') OR 1=1 --"` could result in the application looking up all users
81+
* rather than just one with a matching name.
82+
*/
83+
class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
84+
// The name of the method invoked
85+
private string methodName;
86+
// The zero-indexed position of the SQL fragment sink argument
87+
private int sqlFragmentArgumentIndex;
88+
// The SQL fragment argument itself
89+
private Expr sqlFragmentExpr;
90+
91+
// TODO: `find` with `lock:` option also takes an SQL fragment
92+
PotentiallyUnsafeSqlExecutingMethodCall() {
93+
methodName = this.getMethodName() and
94+
sqlFragmentExpr = this.getArgument(sqlFragmentArgumentIndex) and
95+
methodWithSqlFragmentArg(methodName, sqlFragmentArgumentIndex) and
96+
(
97+
// select only literals containing an interpolated value...
98+
exists(StringInterpolationComponent interpolated |
99+
interpolated = sqlFragmentExpr.(StringlikeLiteral).getComponent(_)
100+
)
101+
or
102+
// ...or string concatenations...
103+
sqlFragmentExpr instanceof AddExpr
104+
or
105+
// ...or variable reads
106+
sqlFragmentExpr instanceof VariableReadAccess
107+
)
108+
}
109+
110+
Expr getSqlFragmentSinkArgument() { result = sqlFragmentExpr }
111+
}
112+
113+
/**
114+
* An `SqlExecution::Range` for an argument to a
115+
* `PotentiallyUnsafeSqlExecutingMethodCall` that may be vulnerable to being
116+
* controlled by user input.
117+
*/
118+
class ActiveRecordSqlExecutionRange extends SqlExecution::Range {
119+
ActiveRecordSqlExecutionRange() {
120+
exists(PotentiallyUnsafeSqlExecutingMethodCall mc |
121+
this.asExpr().getNode() = mc.getSqlFragmentSinkArgument()
122+
)
123+
}
124+
125+
override DataFlow::Node getSql() { result = this }
126+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
activeRecordModelClasses
2+
| ActiveRecordInjection.rb:1:1:3:3 | UserGroup |
3+
| ActiveRecordInjection.rb:5:1:7:3 | User |
4+
| ActiveRecordInjection.rb:9:1:10:3 | Admin |
5+
activeRecordSqlExecutionRanges
6+
| ActiveRecordInjection.rb:22:21:22:41 | "id = #{...}" |
7+
| ActiveRecordInjection.rb:28:16:28:21 | <<-SQL |
8+
| ActiveRecordInjection.rb:32:35:32:60 | "user.id = #{...}" |
9+
| ActiveRecordInjection.rb:45:21:45:33 | ... + ... |
10+
activeRecordModelClassMethodCalls
11+
| ActiveRecordInjection.rb:19:5:19:45 | call to calculate |
12+
| ActiveRecordInjection.rb:22:5:22:42 | call to delete_all |
13+
| ActiveRecordInjection.rb:25:5:25:45 | call to destroy_all |
14+
| ActiveRecordInjection.rb:28:5:28:35 | call to where |
15+
| ActiveRecordInjection.rb:32:5:32:27 | call to joins |
16+
| ActiveRecordInjection.rb:32:5:32:61 | call to where |
17+
| ActiveRecordInjection.rb:45:5:45:34 | call to delete_all |
18+
potentiallyUnsafeSqlExecutingMethodCall
19+
| ActiveRecordInjection.rb:22:5:22:42 | call to delete_all |
20+
| ActiveRecordInjection.rb:28:5:28:35 | call to where |
21+
| ActiveRecordInjection.rb:32:5:32:61 | call to where |
22+
| ActiveRecordInjection.rb:45:5:45:34 | call to delete_all |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import codeql_ruby.controlflow.CfgNodes
2+
import codeql_ruby.frameworks.ActiveRecord
3+
4+
query predicate activeRecordModelClasses(ActiveRecordModelClass cls) { any() }
5+
6+
query predicate activeRecordSqlExecutionRanges(ActiveRecordSqlExecutionRange range) { any() }
7+
8+
query predicate activeRecordModelClassMethodCalls(ActiveRecordModelClassMethodCall call) { any() }
9+
10+
query predicate potentiallyUnsafeSqlExecutingMethodCall(PotentiallyUnsafeSqlExecutingMethodCall call) {
11+
any()
12+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
class UserGroup < ActiveRecord::Base
2+
has_many :users
3+
end
4+
5+
class User < ApplicationRecord
6+
belongs_to :user_group
7+
end
8+
9+
class Admin < User
10+
end
11+
12+
class FooController < ActionController::Base
13+
14+
MAX_USER_ID = 100_000
15+
16+
# A string tainted by user input is inserted into an SQL query
17+
def some_request_handler
18+
# SELECT AVG(#{params[:column]}) FROM "users"
19+
User.calculate(:average, params[:column])
20+
21+
# DELETE FROM "users" WHERE (id = #{params[:id]})
22+
User.delete_all("id = #{params[:id]}")
23+
24+
# SELECT "users".* FROM "users" WHERE (id = #{params[:id]})
25+
User.destroy_all(["id = #{params[:id]}"])
26+
27+
# SELECT "users".* FROM "users" WHERE id BETWEEN #{params[:min_id]} AND 100000
28+
User.where(<<-SQL, MAX_USER_ID)
29+
id BETWEEN #{params[:min_id]} AND ?
30+
SQL
31+
32+
UserGroup.joins(:users).where("user.id = #{params[:id]}")
33+
end
34+
end
35+
36+
37+
class BarController < ApplicationController
38+
39+
def some_other_request_handler
40+
ps = params
41+
42+
uid = ps[:id]
43+
44+
# DELETE FROM "users" WHERE (id = #{uid})
45+
User.delete_all("id = " + uid)
46+
end
47+
48+
end
49+
50+
class BazController < BarController
51+
end

0 commit comments

Comments
 (0)