mirror of
https://github.com/github/codeql.git
synced 2026-04-26 17:25:19 +02:00
Merge pull request #13062 from maikypedia/maikypedia/sqli-sink
Ruby: Add MySQL as SQL Injection Sink
This commit is contained in:
4
ruby/ql/lib/change-notes/2023-05-06-mysql2.md
Normal file
4
ruby/ql/lib/change-notes/2023-05-06-mysql2.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* 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.
|
||||
@@ -78,6 +78,19 @@ module SqlExecution {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that performs SQL sanitization.
|
||||
*/
|
||||
class SqlSanitization extends DataFlow::Node instanceof SqlSanitization::Range { }
|
||||
|
||||
/** Provides a class for modeling new SQL sanitization APIs. */
|
||||
module SqlSanitization {
|
||||
/**
|
||||
* A data-flow node that performs SQL sanitization.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node { }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that executes a regular expression.
|
||||
*
|
||||
|
||||
@@ -32,5 +32,6 @@ private import codeql.ruby.frameworks.Slim
|
||||
private import codeql.ruby.frameworks.Sinatra
|
||||
private import codeql.ruby.frameworks.Twirp
|
||||
private import codeql.ruby.frameworks.Sqlite3
|
||||
private import codeql.ruby.frameworks.Mysql2
|
||||
private import codeql.ruby.frameworks.Pg
|
||||
private import codeql.ruby.frameworks.Sequel
|
||||
|
||||
73
ruby/ql/lib/codeql/ruby/frameworks/Mysql2.qll
Normal file
73
ruby/ql/lib/codeql/ruby/frameworks/Mysql2.qll
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Provides modeling for mysql2, a Ruby library (gem) for interacting with MySql databases.
|
||||
*/
|
||||
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.Concepts
|
||||
|
||||
/**
|
||||
* Provides modeling for mysql2, a Ruby library (gem) for interacting with MySql databases.
|
||||
*/
|
||||
module Mysql2 {
|
||||
/**
|
||||
* Flow summary for `Mysql2::Client.new()`.
|
||||
*/
|
||||
private class SqlSummary extends SummarizedCallable {
|
||||
SqlSummary() { this = "Mysql2::Client.new()" }
|
||||
|
||||
override MethodCall getACall() { result = any(Mysql2Connection c).asExpr().getExpr() }
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to Mysql2::Client.new() is used to establish a connection to a MySql database. */
|
||||
private class Mysql2Connection extends DataFlow::CallNode {
|
||||
Mysql2Connection() {
|
||||
this = API::getTopLevelMember("Mysql2").getMember("Client").getAnInstantiation()
|
||||
}
|
||||
}
|
||||
|
||||
/** A call that executes SQL statements against a MySQL database. */
|
||||
private class Mysql2Execution extends SqlExecution::Range, DataFlow::CallNode {
|
||||
private DataFlow::Node query;
|
||||
|
||||
Mysql2Execution() {
|
||||
exists(Mysql2Connection mysql2Connection |
|
||||
this = mysql2Connection.getAMethodCall("query") and query = this.getArgument(0)
|
||||
or
|
||||
exists(DataFlow::CallNode prepareCall |
|
||||
prepareCall = mysql2Connection.getAMethodCall("prepare") and
|
||||
query = prepareCall.getArgument(0) and
|
||||
this = prepareCall.getAMethodCall("execute")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result = query }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Mysql2::Client.escape`, considered as a sanitizer for SQL statements.
|
||||
*/
|
||||
private class Mysql2EscapeSanitization extends SqlSanitization::Range {
|
||||
Mysql2EscapeSanitization() {
|
||||
this = API::getTopLevelMember("Mysql2").getMember("Client").getAMethodCall("escape")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow summary for `Mysql2::Client.escape()`.
|
||||
*/
|
||||
private class EscapeSummary extends SummarizedCallable {
|
||||
EscapeSummary() { this = "Mysql2::Client.escape()" }
|
||||
|
||||
override MethodCall getACall() { result = any(Mysql2EscapeSanitization c).asExpr().getExpr() }
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,26 @@ module Sqlite3 {
|
||||
|
||||
override DataFlow::Node getSql() { result = this.getArgument(0) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `SQLite3::Database.quote`, considered as a sanitizer for SQL statements.
|
||||
*/
|
||||
private class SQLite3QuoteSanitization extends SqlSanitization {
|
||||
SQLite3QuoteSanitization() {
|
||||
this = API::getTopLevelMember("SQLite3").getMember("Database").getAMethodCall("quote")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow summary for `SQLite3::Database.quote()`.
|
||||
*/
|
||||
private class QuoteSummary extends SummarizedCallable {
|
||||
QuoteSummary() { this = "SQLite3::Database.quote()" }
|
||||
|
||||
override MethodCall getACall() { result = any(SQLite3QuoteSanitization c).asExpr().getExpr() }
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting SQL injection
|
||||
@@ -53,4 +54,6 @@ module SqlInjection {
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
|
||||
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
|
||||
}
|
||||
|
||||
@@ -2814,7 +2814,10 @@
|
||||
| file://:0:0:0:0 | parameter position 0 of File.realdirpath | file://:0:0:0:0 | [summary] to write: return (return) in File.realdirpath |
|
||||
| file://:0:0:0:0 | parameter position 0 of File.realpath | file://:0:0:0:0 | [summary] to write: return (return) in File.realpath |
|
||||
| file://:0:0:0:0 | parameter position 0 of Hash[] | file://:0:0:0:0 | [summary] read: argument position 0.any element in Hash[] |
|
||||
| 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() |
|
||||
| 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() |
|
||||
| file://:0:0:0:0 | parameter position 0 of PG.new() | file://:0:0:0:0 | [summary] to write: return (return) in PG.new() |
|
||||
| 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() |
|
||||
| file://:0:0:0:0 | parameter position 0 of Sequel.connect | file://:0:0:0:0 | [summary] to write: return (return) in Sequel.connect |
|
||||
| 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 |
|
||||
| file://:0:0:0:0 | parameter position 0 of \| | file://:0:0:0:0 | [summary] read: argument position 0.any element in \| |
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
| Mysql2.rb:10:16:10:48 | call to query | Mysql2.rb:10:27:10:47 | "SELECT * FROM users" |
|
||||
| Mysql2.rb:13:16:13:73 | call to query | Mysql2.rb:13:27:13:72 | "SELECT * FROM users WHERE use..." |
|
||||
| Mysql2.rb:17:16:17:76 | call to query | Mysql2.rb:17:27:17:75 | "SELECT * FROM users WHERE use..." |
|
||||
| Mysql2.rb:21:16:21:57 | call to execute | Mysql2.rb:20:31:20:82 | "SELECT * FROM users WHERE id ..." |
|
||||
| Mysql2.rb:25:16:25:60 | call to execute | Mysql2.rb:24:31:24:93 | "SELECT * FROM users WHERE use..." |
|
||||
5
ruby/ql/test/library-tests/frameworks/mysql2/Mysql2.ql
Normal file
5
ruby/ql/test/library-tests/frameworks/mysql2/Mysql2.ql
Normal file
@@ -0,0 +1,5 @@
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.frameworks.Mysql2
|
||||
|
||||
query predicate mysql2SqlExecution(SqlExecution e, DataFlow::Node sql) { sql = e.getSql() }
|
||||
30
ruby/ql/test/library-tests/frameworks/mysql2/Mysql2.rb
Normal file
30
ruby/ql/test/library-tests/frameworks/mysql2/Mysql2.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
class UsersController < ActionController::Base
|
||||
def mysql2_handler(event:, context:)
|
||||
name = params[:user_name]
|
||||
|
||||
conn = Mysql2::Client.new(
|
||||
host: "127.0.0.1",
|
||||
username: "root"
|
||||
)
|
||||
# GOOD: SQL statement is not constructed from user input
|
||||
results1 = conn.query("SELECT * FROM users")
|
||||
|
||||
# BAD: SQL statement constructed from user input
|
||||
results2 = conn.query("SELECT * FROM users WHERE username='#{name}'")
|
||||
|
||||
# GOOD: user input is escaped
|
||||
escaped = Mysql2::Client.escape(name)
|
||||
results3 = conn.query("SELECT * FROM users WHERE username='#{escaped}'")
|
||||
|
||||
# GOOD: user input is escaped
|
||||
statement1 = conn.prepare("SELECT * FROM users WHERE id >= ? AND username = ?")
|
||||
results4 = statement1.execute(1, name, :as => :array)
|
||||
|
||||
# BAD: SQL statement constructed from user input
|
||||
statement2 = conn.prepare("SELECT * FROM users WHERE username='#{name}' AND password = ?")
|
||||
results4 = statement2.execute("password", :as => :array)
|
||||
|
||||
# NOT EXECUTED
|
||||
statement3 = conn.prepare("SELECT * FROM users WHERE username = ?")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user