Skip to content

Commit c95cf5a

Browse files
authored
Merge pull request github#13062 from maikypedia/maikypedia/sqli-sink
Ruby: Add MySQL as SQL Injection Sink
2 parents 219ec9d + 6fa9e13 commit c95cf5a

File tree

10 files changed

+159
-0
lines changed

10 files changed

+159
-0
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.

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ module SqlExecution {
7878
}
7979
}
8080

81+
/**
82+
* A data-flow node that performs SQL sanitization.
83+
*/
84+
class SqlSanitization extends DataFlow::Node instanceof SqlSanitization::Range { }
85+
86+
/** Provides a class for modeling new SQL sanitization APIs. */
87+
module SqlSanitization {
88+
/**
89+
* A data-flow node that performs SQL sanitization.
90+
*/
91+
abstract class Range extends DataFlow::Node { }
92+
}
93+
8194
/**
8295
* A data-flow node that executes a regular expression.
8396
*

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ private import codeql.ruby.frameworks.Slim
3232
private import codeql.ruby.frameworks.Sinatra
3333
private import codeql.ruby.frameworks.Twirp
3434
private import codeql.ruby.frameworks.Sqlite3
35+
private import codeql.ruby.frameworks.Mysql2
3536
private import codeql.ruby.frameworks.Pg
3637
private import codeql.ruby.frameworks.Sequel
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 |
39+
this = mysql2Connection.getAMethodCall("query") and query = this.getArgument(0)
40+
or
41+
exists(DataFlow::CallNode prepareCall |
42+
prepareCall = mysql2Connection.getAMethodCall("prepare") and
43+
query = prepareCall.getArgument(0) and
44+
this = prepareCall.getAMethodCall("execute")
45+
)
46+
)
47+
}
48+
49+
override DataFlow::Node getSql() { result = query }
50+
}
51+
52+
/**
53+
* A call to `Mysql2::Client.escape`, considered as a sanitizer for SQL statements.
54+
*/
55+
private class Mysql2EscapeSanitization extends SqlSanitization::Range {
56+
Mysql2EscapeSanitization() {
57+
this = API::getTopLevelMember("Mysql2").getMember("Client").getAMethodCall("escape")
58+
}
59+
}
60+
61+
/**
62+
* Flow summary for `Mysql2::Client.escape()`.
63+
*/
64+
private class EscapeSummary extends SummarizedCallable {
65+
EscapeSummary() { this = "Mysql2::Client.escape()" }
66+
67+
override MethodCall getACall() { result = any(Mysql2EscapeSanitization c).asExpr().getExpr() }
68+
69+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
70+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
71+
}
72+
}
73+
}

ruby/ql/lib/codeql/ruby/frameworks/Sqlite3.qll

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,26 @@ module Sqlite3 {
7777

7878
override DataFlow::Node getSql() { result = this.getArgument(0) }
7979
}
80+
81+
/**
82+
* A call to `SQLite3::Database.quote`, considered as a sanitizer for SQL statements.
83+
*/
84+
private class SQLite3QuoteSanitization extends SqlSanitization {
85+
SQLite3QuoteSanitization() {
86+
this = API::getTopLevelMember("SQLite3").getMember("Database").getAMethodCall("quote")
87+
}
88+
}
89+
90+
/**
91+
* Flow summary for `SQLite3::Database.quote()`.
92+
*/
93+
private class QuoteSummary extends SummarizedCallable {
94+
QuoteSummary() { this = "SQLite3::Database.quote()" }
95+
96+
override MethodCall getACall() { result = any(SQLite3QuoteSanitization c).asExpr().getExpr() }
97+
98+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
99+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
100+
}
101+
}
80102
}

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

Lines changed: 3 additions & 0 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
@@ -53,4 +54,6 @@ module SqlInjection {
5354
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
5455
StringConstArrayInclusionCallBarrier
5556
{ }
57+
58+
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
5659
}

ruby/ql/test/library-tests/dataflow/local/TaintStep.expected

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,7 +2814,10 @@
28142814
| file://:0:0:0:0 | parameter position 0 of File.realdirpath | file://:0:0:0:0 | [summary] to write: return (return) in File.realdirpath |
28152815
| file://:0:0:0:0 | parameter position 0 of File.realpath | file://:0:0:0:0 | [summary] to write: return (return) in File.realpath |
28162816
| file://:0:0:0:0 | parameter position 0 of Hash[] | file://:0:0:0:0 | [summary] read: argument position 0.any element in Hash[] |
2817+
| file://:0:0:0:0 | parameter position 0 of Mysql2::Client.escape() | file://:0:0:0:0 | [summary] to write: return (return) in Mysql2::Client.escape() |
2818+
| file://:0:0:0:0 | parameter position 0 of Mysql2::Client.new() | file://:0:0:0:0 | [summary] to write: return (return) in Mysql2::Client.new() |
28172819
| file://:0:0:0:0 | parameter position 0 of PG.new() | file://:0:0:0:0 | [summary] to write: return (return) in PG.new() |
2820+
| file://:0:0:0:0 | parameter position 0 of SQLite3::Database.quote() | file://:0:0:0:0 | [summary] to write: return (return) in SQLite3::Database.quote() |
28182821
| file://:0:0:0:0 | parameter position 0 of Sequel.connect | file://:0:0:0:0 | [summary] to write: return (return) in Sequel.connect |
28192822
| file://:0:0:0:0 | parameter position 0 of String.try_convert | file://:0:0:0:0 | [summary] to write: return (return) in String.try_convert |
28202823
| file://:0:0:0:0 | parameter position 0 of \| | file://:0:0:0:0 | [summary] read: argument position 0.any element in \| |
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)