|
| 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 | +} |
0 commit comments