From ad5355a04af119ad3de4acb907db42f507705698 Mon Sep 17 00:00:00 2001 From: Maiky <76447395+maikypedia@users.noreply.github.com> Date: Tue, 23 May 2023 19:49:03 +0200 Subject: [PATCH] Pg Library, change note and Frameworks.qll --- ruby/ql/lib/change-notes/2023-05-06-pg.md | 4 + ruby/ql/lib/codeql/ruby/Frameworks.qll | 1 + ruby/ql/lib/codeql/ruby/frameworks/Pg.qll | 77 +++++++++++++++++++ .../security/cwe-089/PgInjection.rb | 70 +++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 ruby/ql/lib/change-notes/2023-05-06-pg.md create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/Pg.qll create mode 100644 ruby/ql/test/query-tests/security/cwe-089/PgInjection.rb diff --git a/ruby/ql/lib/change-notes/2023-05-06-pg.md b/ruby/ql/lib/change-notes/2023-05-06-pg.md new file mode 100644 index 00000000000..1828497c04e --- /dev/null +++ b/ruby/ql/lib/change-notes/2023-05-06-pg.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Support for the `pg` gem has been added. Method calls that execute queries against an PostgreSQL database that may be vulnerable to injection attacks will now be recognized. \ No newline at end of file diff --git a/ruby/ql/lib/codeql/ruby/Frameworks.qll b/ruby/ql/lib/codeql/ruby/Frameworks.qll index e61ac723e7e..29eacf22e33 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.Pg diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Pg.qll b/ruby/ql/lib/codeql/ruby/frameworks/Pg.qll new file mode 100644 index 00000000000..27f4844bd77 --- /dev/null +++ b/ruby/ql/lib/codeql/ruby/frameworks/Pg.qll @@ -0,0 +1,77 @@ +/** + * Provides modeling for Pg, a Ruby library (gem) for interacting with PostgreSQL databases. + */ + +private import codeql.ruby.ApiGraphs +private import codeql.ruby.dataflow.FlowSummary +private import codeql.ruby.Concepts + +/** + * Provides modeling for Pg, a Ruby library (gem) for interacting with PostgreSQL databases. + */ +module Pg { + /** + * Flow summary for `PG.new()`. This method initializes a database connection. + */ + private class SqlSummary extends SummarizedCallable { + SqlSummary() { this = "PG.new()" } + + override MethodCall getACall() { result = any(PgConnection c).asExpr().getExpr() } + + override predicate propagatesFlowExt(string input, string output, boolean preservesValue) { + input = "Argument[0]" and output = "ReturnValue" and preservesValue = false + } + } + + /** A call to PG::Connection.open() is used to establish a connection to a PostgreSQL database. */ + private class PgConnection extends DataFlow::CallNode { + PgConnection() { + this = + API::getTopLevelMember("PG") + .getMember("Connection") + .getAMethodCall(["open", "new", "connect_start"]) + or + this = API::getTopLevelMember("PG").getAnInstantiation() + } + } + + /** A call that prepares an SQL statment to be executed later. */ + private class PgPrepareCall extends SqlConstruction::Range, DataFlow::CallNode { + private DataFlow::Node query; + private PgConnection pgConnection; + private string queryName; + + PgPrepareCall() { + this = pgConnection.getAMethodCall("prepare") and + queryName = this.getArgument(0).getConstantValue().getStringlikeValue() and + query = this.getArgument(1) + } + + PgConnection getConnection() { result = pgConnection } + + string getQueryName() { result = queryName } + + override DataFlow::Node getSql() { result = query } + } + + /** A call that executes SQL statements against a PostgreSQL database. */ + private class PgExecution extends SqlExecution::Range, DataFlow::CallNode { + private DataFlow::Node query; + + PgExecution() { + exists(PgConnection pgConnection | + this = + pgConnection.getAMethodCall(["exec", "async_exec", "exec_params", "async_exec_params"]) and + query = this.getArgument(0) + or + exists(PgPrepareCall prepareCall | + pgConnection = prepareCall.getConnection() and + this.getArgument(0).getConstantValue().isStringlikeValue(prepareCall.getQueryName()) and + query = prepareCall.getSql() + ) + ) + } + + override DataFlow::Node getSql() { result = query } + } +} diff --git a/ruby/ql/test/query-tests/security/cwe-089/PgInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/PgInjection.rb new file mode 100644 index 00000000000..549be489858 --- /dev/null +++ b/ruby/ql/test/query-tests/security/cwe-089/PgInjection.rb @@ -0,0 +1,70 @@ +class FooController < ActionController::Base + + def some_request_handler + # A string tainted by user input is inserted into a query + # (i.e a remote flow source) + name = params[:name] + + # Establish a connection to a PostgreSQL database + conn = PG::Connection.open(:dbname => 'postgresql', :user => 'user', :password => 'pass', :host => 'localhost', :port => '5432') + + # .exec() and .async_exec() + # BAD: SQL statement constructed from user input + qry1 = "SELECT * FROM users WHERE username = '#{name}';" + conn.exec(qry1) + conn.async_exec(qry1) + + # .exec_params() and .async_exec_params() + # BAD: SQL statement constructed from user input + qry2 = "SELECT * FROM users WHERE username = '#{name}';" + conn.exec_params(qry2) + conn.async_exec_params(qry2) + + # .exec_params() and .async_exec_params() + # GOOD: SQL statement constructed from sanitized user input + qry2 = "SELECT * FROM users WHERE username = $1;" + conn.exec_params(qry2, [name]) + conn.async_exec_params(qry2, [name]) + + # .prepare() and .exec_prepared() + # BAD: SQL statement constructed from user input + qry3 = "SELECT * FROM users WHERE username = '#{name}';" + conn.prepare("query_1", qry3) + conn.exec_prepared('query_1') + + # .prepare() and .exec_prepared() + # GOOD: SQL statement constructed from sanitized user input + qry3 = "SELECT * FROM users WHERE username = $1;" + conn.prepare("query_2", qry3) + conn.exec_prepared('query_2', [name]) + + # .prepare() and .exec_prepared() + # NOT EXECUTED: SQL statement constructed from user input but not executed + qry3 = "SELECT * FROM users WHERE username = '#{name}';" + conn.prepare("query_3", qry3) + end +end + +class BarController < ApplicationController + def safe_paths + name1 = params["name1"] + # GOOD: barrier guard prevents taint flow + if name == "admin" + qry_bar1 = "SELECT * FROM users WHERE username = '%s';" % name + else + qry_bar1 = "SELECT * FROM users WHERE username = 'none';" + end + conn.exec_params(qry_bar1) + + + name2 = params["name2"] + # GOOD: barrier guard prevents taint flow + name2 = if ["admin", "guest"].include? name2 + name2 + else + name2 = "none" + end + qry_bar2 = "SELECT * FROM users WHERE username = '%s';" % name + conn.exec_params(qry_bar2) + end +end