Skip to content

Commit 6a3d995

Browse files
committed
Add Mysql2 as SQL Injection Sink
1 parent 7323d4e commit 6a3d995

File tree

6 files changed

+113
-2
lines changed

6 files changed

+113
-2
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+
* Support for the `mysql2` gem has been added. Method calls that execute queries against an MySQL database that may be vulnerable to injection attacks will now be recognized.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Provides modeling for mysql2, a Ruby library (gem) for interacting with MySql databases.
3+
*/
4+
5+
private import codeql.ruby.ApiGraphs
6+
private import codeql.ruby.dataflow.FlowSummary
7+
private import codeql.ruby.Concepts
8+
9+
/**
10+
* Provides modeling for mysql2, a Ruby library (gem) for interacting with MySql databases.
11+
*/
12+
module Mysql2 {
13+
/**
14+
* Flow summary for `Mysql2::Client.new()`.
15+
*/
16+
private class SqlSummary extends SummarizedCallable {
17+
SqlSummary() { this = "Mysql2::Client.new()" }
18+
19+
override MethodCall getACall() { result = any(Mysql2Connection c).asExpr().getExpr() }
20+
21+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
22+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
23+
}
24+
}
25+
26+
/** A call to Mysql2::Client.new() is used to establish a connection to a MySql database. */
27+
private class Mysql2Connection extends DataFlow::CallNode {
28+
Mysql2Connection() {
29+
this = API::getTopLevelMember("Mysql2").getMember("Client").getAnInstantiation()
30+
}
31+
}
32+
33+
/** A call that executes SQL statements against a MySQL database. */
34+
private class Mysql2Execution extends SqlExecution::Range, DataFlow::CallNode {
35+
private DataFlow::Node query;
36+
37+
Mysql2Execution() {
38+
exists(Mysql2Connection mysql2Connection, DataFlow::CallNode prepareCall |
39+
this = mysql2Connection.getAMethodCall("query") and query = this.getArgument(0)
40+
or
41+
prepareCall = mysql2Connection.getAMethodCall("prepare") and
42+
query = prepareCall.getArgument(0) and
43+
this = prepareCall.getAMethodCall("execute")
44+
)
45+
}
46+
47+
override DataFlow::Node getSql() { result = query }
48+
}
49+
}

ruby/ql/lib/codeql/ruby/security/SqlInjectionCustomizations.qll

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ private import codeql.ruby.Concepts
77
private import codeql.ruby.DataFlow
88
private import codeql.ruby.dataflow.BarrierGuards
99
private import codeql.ruby.dataflow.RemoteFlowSources
10+
private import codeql.ruby.ApiGraphs
1011

1112
/**
1213
* Provides default sources, sinks and sanitizers for detecting SQL injection
@@ -51,6 +52,23 @@ module SqlInjection {
5152
* sanitizer-guard.
5253
*/
5354
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
54-
StringConstArrayInclusionCallBarrier
55-
{ }
55+
StringConstArrayInclusionCallBarrier { }
56+
57+
/**
58+
* A call to `Mysql2::Client.escape`, considered as a sanitizer.
59+
*/
60+
class Mysql2EscapeSanitization extends Sanitizer {
61+
Mysql2EscapeSanitization() {
62+
this = API::getTopLevelMember("Mysql2").getMember("Client").getAMethodCall("escape")
63+
}
64+
}
65+
66+
/**
67+
* A call to `SQLite3::Database.quote`, considered as a sanitizer.
68+
*/
69+
class SQLite3EscapeSanitization extends Sanitizer {
70+
SQLite3EscapeSanitization() {
71+
this = API::getTopLevelMember("SQLite3").getMember("Database").getAMethodCall("quote")
72+
}
73+
}
5674
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| Mysql2.rb:10:16:10:48 | call to query | Mysql2.rb:10:27:10:47 | "SELECT * FROM users" |
2+
| Mysql2.rb:13:16:13:73 | call to query | Mysql2.rb:13:27:13:72 | "SELECT * FROM users WHERE use..." |
3+
| Mysql2.rb:17:16:17:76 | call to query | Mysql2.rb:17:27:17:75 | "SELECT * FROM users WHERE use..." |
4+
| Mysql2.rb:21:16:21:57 | call to execute | Mysql2.rb:20:31:20:82 | "SELECT * FROM users WHERE id ..." |
5+
| Mysql2.rb:25:16:25:60 | call to execute | Mysql2.rb:24:31:24:93 | "SELECT * FROM users WHERE use..." |
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
private import codeql.ruby.DataFlow
2+
private import codeql.ruby.Concepts
3+
private import codeql.ruby.frameworks.Mysql2
4+
5+
query predicate mysql2SqlExecution(SqlExecution e, DataFlow::Node sql) { sql = e.getSql() }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class UsersController < ActionController::Base
2+
def mysql2_handler(event:, context:)
3+
name = params[:user_name]
4+
5+
conn = Mysql2::Client.new(
6+
host: "127.0.0.1",
7+
username: "root"
8+
)
9+
# GOOD: SQL statement is not constructed from user input
10+
results1 = conn.query("SELECT * FROM users")
11+
12+
# BAD: SQL statement constructed from user input
13+
results2 = conn.query("SELECT * FROM users WHERE username='#{name}'")
14+
15+
# GOOD: user input is escaped
16+
escaped = Mysql2::Client.escape(name)
17+
results3 = conn.query("SELECT * FROM users WHERE username='#{escaped}'")
18+
19+
# GOOD: user input is escaped
20+
statement1 = conn.prepare("SELECT * FROM users WHERE id >= ? AND username = ?")
21+
results4 = statement1.execute(1, name, :as => :array)
22+
23+
# BAD: SQL statement constructed from user input
24+
statement2 = conn.prepare("SELECT * FROM users WHERE username='#{name}' AND password = ?")
25+
results4 = statement2.execute("password", :as => :array)
26+
27+
# NOT EXECUTED
28+
statement3 = conn.prepare("SELECT * FROM users WHERE username = ?")
29+
end
30+
end

0 commit comments

Comments
 (0)