diff --git a/ruby/ql/lib/change-notes/2023-05-03-sqlite3.md b/ruby/ql/lib/change-notes/2023-05-03-sqlite3.md new file mode 100644 index 00000000000..16af7f859e9 --- /dev/null +++ b/ruby/ql/lib/change-notes/2023-05-03-sqlite3.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Support for the `sqlite3` gem has been added. Method calls that execute queries against an SQLite3 database that may be vulnerable to injection attacks will now be recognized. diff --git a/ruby/ql/lib/codeql/ruby/Frameworks.qll b/ruby/ql/lib/codeql/ruby/Frameworks.qll index 7f75f889e71..e61ac723e7e 100644 --- a/ruby/ql/lib/codeql/ruby/Frameworks.qll +++ b/ruby/ql/lib/codeql/ruby/Frameworks.qll @@ -31,3 +31,4 @@ private import codeql.ruby.frameworks.Erb private import codeql.ruby.frameworks.Slim private import codeql.ruby.frameworks.Sinatra private import codeql.ruby.frameworks.Twirp +private import codeql.ruby.frameworks.Sqlite3 diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Sqlite3.qll b/ruby/ql/lib/codeql/ruby/frameworks/Sqlite3.qll new file mode 100644 index 00000000000..e051a847993 --- /dev/null +++ b/ruby/ql/lib/codeql/ruby/frameworks/Sqlite3.qll @@ -0,0 +1,80 @@ +/** + * Provides modeling for `sqlite3`, a library that allows Ruby programs to use the SQLite3 database engine. + * Version: 1.6.2 + * https://github.com/sparklemotion/sqlite3-ruby + */ + +private import ruby +private import codeql.ruby.ApiGraphs +private import codeql.ruby.dataflow.FlowSummary +private import codeql.ruby.Concepts + +/** + * Provides modeling for `sqlite3`, a library that allows Ruby programs to use the SQLite3 database engine. + * Version: 1.6.2 + * https://github.com/sparklemotion/sqlite3-ruby + */ +module Sqlite3 { + /** Gets a method call with a receiver that is a database instance. */ + private DataFlow::CallNode getADatabaseMethodCall(string methodName) { + exists(API::Node dbInstance | + dbInstance = API::getTopLevelMember("SQLite3").getMember("Database").getInstance() and + ( + result = dbInstance.getAMethodCall(methodName) + or + // e.g. SQLite3::Database.new("foo.db") |db| { db.some_method } + exists(DataFlow::BlockNode block | + result.getMethodName() = methodName and + block = dbInstance.getAValueReachableFromSource().(DataFlow::CallNode).getBlock() and + block.getParameter(0).flowsTo(result.getReceiver()) + ) + ) + ) + } + + /** A prepared but unexecuted SQL statement. */ + private class PreparedStatement extends SqlConstruction::Range, DataFlow::CallNode { + PreparedStatement() { this = getADatabaseMethodCall("prepare") } + + override DataFlow::Node getSql() { result = this.getArgument(0) } + } + + /** Execution of a prepared SQL statement. */ + private class PreparedStatementExecution extends SqlExecution::Range, DataFlow::CallNode { + private PreparedStatement stmt; + + PreparedStatementExecution() { + stmt.flowsTo(this.getReceiver()) and + this.getMethodName() = ["columns", "execute", "execute!", "get_metadata", "types"] + } + + override DataFlow::Node getSql() { result = stmt.getReceiver() } + } + + /** Gets the name of a method called against a database that executes an SQL statement. */ + private string getAnExecutionMethodName() { + result = + [ + "execute", "execute2", "execute_batch", "execute_batch2", "get_first_row", + "get_first_value", "query" + ] + } + + /** A method call against a database that constructs an SQL query. */ + private class DatabaseMethodCallSqlConstruction extends SqlConstruction::Range, DataFlow::CallNode + { + // Database query execution methods also construct an SQL query + DatabaseMethodCallSqlConstruction() { + this = getADatabaseMethodCall(getAnExecutionMethodName()) + } + + override DataFlow::Node getSql() { result = this.getArgument(0) } + } + + /** A method call against a database that executes an SQL query. */ + private class DatabaseMethodCallSqlExecution extends SqlExecution::Range, DataFlow::CallNode { + DatabaseMethodCallSqlExecution() { this = getADatabaseMethodCall(getAnExecutionMethodName()) } + + override DataFlow::Node getSql() { result = this.getArgument(0) } + } +} diff --git a/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected new file mode 100644 index 00000000000..bd4f9883045 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected @@ -0,0 +1,10 @@ +sqlite3SqlConstruction +| sqlite3.rb:5:1:5:17 | call to execute | sqlite3.rb:5:12:5:17 | <<-SQL | +| sqlite3.rb:12:8:12:41 | call to prepare | sqlite3.rb:12:19:12:41 | "select * from numbers" | +| sqlite3.rb:17:3:19:5 | call to execute | sqlite3.rb:17:15:17:35 | "select * from table" | +| sqlite3.rb:29:7:29:40 | call to execute | sqlite3.rb:29:19:29:39 | "select * from table" | +sqlite3SqlExecution +| sqlite3.rb:5:1:5:17 | call to execute | sqlite3.rb:5:12:5:17 | <<-SQL | +| sqlite3.rb:14:1:14:12 | call to execute | sqlite3.rb:12:8:12:9 | db | +| sqlite3.rb:17:3:19:5 | call to execute | sqlite3.rb:17:15:17:35 | "select * from table" | +| sqlite3.rb:29:7:29:40 | call to execute | sqlite3.rb:29:19:29:39 | "select * from table" | diff --git a/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.ql b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.ql new file mode 100644 index 00000000000..1cb2d7004cc --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.ql @@ -0,0 +1,7 @@ +private import codeql.ruby.DataFlow +private import codeql.ruby.Concepts +private import codeql.ruby.frameworks.Sqlite3 + +query predicate sqlite3SqlConstruction(SqlConstruction c, DataFlow::Node sql) { sql = c.getSql() } + +query predicate sqlite3SqlExecution(SqlExecution e, DataFlow::Node sql) { sql = e.getSql() } diff --git a/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb b/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb new file mode 100644 index 00000000000..465bb708598 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb @@ -0,0 +1,31 @@ +require 'sqlite3' + +db = SQLite3::Database.new "test.db" + +db.execute <<-SQL + create table numbers ( + name varchar(30), + val int + ); +SQL + +stmt = db.prepare "select * from numbers" + +stmt.execute + +SQLite3::Database.new( "data.db" ) do |db| + db.execute( "select * from table" ) do |row| + p row + end +end + + +class MyDatabaseWrapper + def initialize(filename) + @db = SQLite3::Database.new(filename, results_as_hash: true) + end + + def select_rows(category) + @db.execute("select * from table") + end +end