From 3960853af0962bf8512d0465bcdae0bdfa3ee4f4 Mon Sep 17 00:00:00 2001 From: Maiky <76447395+maikypedia@users.noreply.github.com> Date: Sun, 7 May 2023 23:56:56 +0200 Subject: [PATCH] CWE-089 Add Sequel SQL Injection Sink --- ruby/ql/lib/change-notes/2023-05-07-sequel.md | 4 ++ ruby/ql/lib/codeql/ruby/Frameworks.qll | 1 + ruby/ql/lib/codeql/ruby/frameworks/Sequel.qll | 71 +++++++++++++++++++ .../frameworks/sequel/Sequel.expected | 23 ++++++ .../library-tests/frameworks/sequel/Sequel.ql | 7 ++ .../library-tests/frameworks/sequel/sequel.rb | 67 +++++++++++++++++ 6 files changed, 173 insertions(+) create mode 100644 ruby/ql/lib/change-notes/2023-05-07-sequel.md create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/Sequel.qll create mode 100644 ruby/ql/test/library-tests/frameworks/sequel/Sequel.expected create mode 100644 ruby/ql/test/library-tests/frameworks/sequel/Sequel.ql create mode 100644 ruby/ql/test/library-tests/frameworks/sequel/sequel.rb diff --git a/ruby/ql/lib/change-notes/2023-05-07-sequel.md b/ruby/ql/lib/change-notes/2023-05-07-sequel.md new file mode 100644 index 00000000000..3688f28db56 --- /dev/null +++ b/ruby/ql/lib/change-notes/2023-05-07-sequel.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Support for the `sequel` gem has been added. Method calls that execute queries against a 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 e61ac723e7e..d7b76c090b2 100644 --- a/ruby/ql/lib/codeql/ruby/Frameworks.qll +++ b/ruby/ql/lib/codeql/ruby/Frameworks.qll @@ -32,3 +32,4 @@ 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.Sequel diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Sequel.qll b/ruby/ql/lib/codeql/ruby/frameworks/Sequel.qll new file mode 100644 index 00000000000..b9488a92016 --- /dev/null +++ b/ruby/ql/lib/codeql/ruby/frameworks/Sequel.qll @@ -0,0 +1,71 @@ +/** + * Provides modeling for `Sequel`, the database toolkit for Ruby. + * https://github.com/jeremyevans/sequel + */ + +private import ruby +private import codeql.ruby.ApiGraphs +private import codeql.ruby.dataflow.FlowSummary +private import codeql.ruby.Concepts + +/** + * Provides modeling for `Sequel`, the database toolkit for Ruby. + * https://github.com/jeremyevans/sequel + */ +module Sequel { + /** Flow Summary for `Sequel`. */ + private class SqlSummary extends SummarizedCallable { + SqlSummary() { this = "Sequel.connect" } + + override MethodCall getACall() { result = any(SequelConnection c).asExpr().getExpr() } + + override predicate propagatesFlowExt(string input, string output, boolean preservesValue) { + input = "Argument[0]" and output = "ReturnValue" and preservesValue = false + } + } + + /** A call to establish a connection to a database */ + private class SequelConnection extends DataFlow::CallNode { + SequelConnection() { + this = + API::getTopLevelMember("Sequel").getAMethodCall(["connect", "sqlite", "mysql2", "jdbc"]) + } + } + + /** A call that constructs SQL statements */ + private class SequelConstruction extends SqlConstruction::Range, DataFlow::CallNode { + DataFlow::Node query; + + SequelConstruction() { + this = API::getTopLevelMember("Sequel").getAMethodCall("cast") and query = this.getArgument(1) + or + this = API::getTopLevelMember("Sequel").getAMethodCall("function") and + query = this.getArgument(0) + } + + override DataFlow::Node getSql() { result = query } + } + + /** A call that executes SQL statements against a database */ + private class SequelExecution extends SqlExecution::Range, DataFlow::CallNode { + SequelExecution() { + exists(SequelConnection sequelConnection | + this = + sequelConnection + .getAMethodCall([ + "execute", "execute_ddl", "execute_dui", "execute_insert", "run", "<<", "fetch", + "fetch_rows", "[]", "log_connection_yield" + ]) or + this = + sequelConnection + .getAMethodCall("dataset") + .getAMethodCall([ + "with_sql", "with_sql_all", "with_sql_delete", "with_sql_each", "with_sql_first", + "with_sql_insert", "with_sql_single_value", "with_sql_update" + ]) + ) + } + + override DataFlow::Node getSql() { result = this.getArgument(0) } + } +} diff --git a/ruby/ql/test/library-tests/frameworks/sequel/Sequel.expected b/ruby/ql/test/library-tests/frameworks/sequel/Sequel.expected new file mode 100644 index 00000000000..b44d06e6c19 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sequel/Sequel.expected @@ -0,0 +1,23 @@ +sequelSqlConstruction +| sequel.rb:63:29:63:49 | call to cast | sequel.rb:63:45:63:48 | name | +| sequel.rb:66:29:66:49 | call to function | sequel.rb:66:45:66:48 | name | +sequelSqlExecution +| sequel.rb:10:9:10:60 | ...[...] | sequel.rb:10:14:10:59 | "SELECT * FROM users WHERE use..." | +| sequel.rb:13:9:13:64 | call to run | sequel.rb:13:18:13:63 | "SELECT * FROM users WHERE use..." | +| sequel.rb:16:9:18:11 | call to fetch | sequel.rb:16:20:16:65 | "SELECT * FROM users WHERE use..." | +| sequel.rb:21:9:21:65 | ...[...] | sequel.rb:21:14:21:64 | "SELECT * FROM users WHERE use..." | +| sequel.rb:24:9:24:65 | call to execute | sequel.rb:24:22:24:65 | "SELECT * FROM users WHERE use..." | +| sequel.rb:27:9:27:71 | call to execute_ddl | sequel.rb:27:26:27:71 | "SELECT * FROM users WHERE use..." | +| sequel.rb:30:9:30:71 | call to execute_dui | sequel.rb:30:26:30:71 | "SELECT * FROM users WHERE use..." | +| sequel.rb:33:9:33:74 | call to execute_insert | sequel.rb:33:29:33:74 | "SELECT * FROM users WHERE use..." | +| sequel.rb:36:9:36:62 | ... << ... | sequel.rb:36:17:36:62 | "SELECT * FROM users WHERE use..." | +| sequel.rb:39:9:39:79 | call to fetch_rows | sequel.rb:39:25:39:70 | "SELECT * FROM users WHERE use..." | +| sequel.rb:42:9:42:81 | call to with_sql_all | sequel.rb:42:35:42:80 | "SELECT * FROM users WHERE use..." | +| sequel.rb:45:9:45:84 | call to with_sql_delete | sequel.rb:45:38:45:83 | "SELECT * FROM users WHERE use..." | +| sequel.rb:48:9:48:90 | call to with_sql_each | sequel.rb:48:36:48:81 | "SELECT * FROM users WHERE use..." | +| sequel.rb:51:9:51:83 | call to with_sql_first | sequel.rb:51:37:51:82 | "SELECT * FROM users WHERE use..." | +| sequel.rb:54:9:54:84 | call to with_sql_insert | sequel.rb:54:38:54:83 | "SELECT * FROM users WHERE use..." | +| sequel.rb:57:9:57:90 | call to with_sql_single_value | sequel.rb:57:44:57:89 | "SELECT * FROM users WHERE use..." | +| sequel.rb:60:9:60:84 | call to with_sql_update | sequel.rb:60:38:60:83 | "SELECT * FROM users WHERE use..." | +| sequel.rb:63:9:63:20 | ...[...] | sequel.rb:63:14:63:19 | :table | +| sequel.rb:66:9:66:20 | ...[...] | sequel.rb:66:14:66:19 | :table | diff --git a/ruby/ql/test/library-tests/frameworks/sequel/Sequel.ql b/ruby/ql/test/library-tests/frameworks/sequel/Sequel.ql new file mode 100644 index 00000000000..9645c5d4f17 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sequel/Sequel.ql @@ -0,0 +1,7 @@ +private import codeql.ruby.DataFlow +private import codeql.ruby.Concepts +private import codeql.ruby.frameworks.Sequel + +query predicate sequelSqlConstruction(SqlConstruction c, DataFlow::Node sql) { sql = c.getSql() } + +query predicate sequelSqlExecution(SqlExecution e, DataFlow::Node sql) { sql = e.getSql() } diff --git a/ruby/ql/test/library-tests/frameworks/sequel/sequel.rb b/ruby/ql/test/library-tests/frameworks/sequel/sequel.rb new file mode 100644 index 00000000000..d760f6c3d07 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/sequel/sequel.rb @@ -0,0 +1,67 @@ +require 'sequel' + +class UsersController < ActionController::Base + def sequel_handler(event:, context:) + name = params[:name] + conn = Sequel.sqlite("sqlite://example.db") + + # BAD: SQL statement constructed from user input + conn["SELECT * FROM users WHERE username='#{name}'"] + + # BAD: SQL statement constructed from user input + conn.run("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.fetch("SELECT * FROM users WHERE username='#{name}'") do |row| + puts row[:name] + end + + # GOOD: SQL statement is not constructed from user input + conn["SELECT * FROM users WHERE username='im_not_input'"] + + # BAD: SQL statement constructed from user input + conn.execute "SELECT * FROM users WHERE username=#{name}" + + # BAD: SQL statement constructed from user input + conn.execute_ddl "SELECT * FROM users WHERE username='#{name}'" + + # BAD: SQL statement constructed from user input + conn.execute_dui "SELECT * FROM users WHERE username='#{name}'" + + # BAD: SQL statement constructed from user input + conn.execute_insert "SELECT * FROM users WHERE username='#{name}'" + + # BAD: SQL statement constructed from user input + conn << "SELECT * FROM users WHERE username='#{name}'" + + # BAD: SQL statement constructed from user input + conn.fetch_rows("SELECT * FROM users WHERE username='#{name}'"){|row| } + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_all("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_delete("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_each("SELECT * FROM users WHERE username='#{name}'"){|row| } + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_first("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_insert("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_single_value("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn.dataset.with_sql_update("SELECT * FROM users WHERE username='#{name}'") + + # BAD: SQL statement constructed from user input + conn[:table].select(Sequel.cast(:a, name)) + + # BAD: SQL statement constructed from user input + conn[:table].select(Sequel.function(name)) + end +end \ No newline at end of file