From 9dc04f30ac2643ccc02c670c8dda44cf62cb2c4b Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Mon, 17 Apr 2023 00:35:50 +0100 Subject: [PATCH 1/4] Ruby: model sqlite3 --- .../ql/lib/codeql/ruby/frameworks/Sqlite3.qll | 80 +++++++++++++++++++ .../frameworks/sqlite3/Sqlite3.expected | 8 ++ .../frameworks/sqlite3/Sqlite3.ql | 7 ++ .../frameworks/sqlite3/sqlite3.rb | 20 +++++ 4 files changed, 115 insertions(+) create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/Sqlite3.qll create mode 100644 ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected create mode 100644 ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.ql create mode 100644 ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb 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..49bec595341 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected @@ -0,0 +1,8 @@ +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" | +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" | 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..0a63dc79b35 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb @@ -0,0 +1,20 @@ +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 From edf48f4839e6c81f8ece1acad78437b2f85af983 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Mon, 24 Apr 2023 09:11:14 +0100 Subject: [PATCH 2/4] Ruby: add sqlite3 to Frameworks.qll --- ruby/ql/lib/codeql/ruby/Frameworks.qll | 1 + 1 file changed, 1 insertion(+) 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 From a26f9736f134ee32f4c1c6962f879c9d238b2360 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Wed, 3 May 2023 15:12:06 +0100 Subject: [PATCH 3/4] Ruby: add change note for sqlite3 support --- ruby/ql/lib/change-notes/2023-05-03-sqlite3.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 ruby/ql/lib/change-notes/2023-05-03-sqlite3.md 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. From 6e6eee2dab081f18c4fb572d6f0a346c902dbaa8 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Wed, 3 May 2023 15:15:51 +0100 Subject: [PATCH 4/4] Ruby: add test case for instance variable flow with sqlite3 --- .../library-tests/frameworks/sqlite3/Sqlite3.expected | 2 ++ .../test/library-tests/frameworks/sqlite3/sqlite3.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected index 49bec595341..bd4f9883045 100644 --- a/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/Sqlite3.expected @@ -2,7 +2,9 @@ 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.rb b/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb index 0a63dc79b35..465bb708598 100644 --- a/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb +++ b/ruby/ql/test/library-tests/frameworks/sqlite3/sqlite3.rb @@ -18,3 +18,14 @@ SQLite3::Database.new( "data.db" ) do |db| 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