Skip to content

Commit 3960853

Browse files
committed
CWE-089 Add Sequel SQL Injection Sink
1 parent 7323d4e commit 3960853

File tree

6 files changed

+173
-0
lines changed

6 files changed

+173
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* 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.

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ private import codeql.ruby.frameworks.Slim
3232
private import codeql.ruby.frameworks.Sinatra
3333
private import codeql.ruby.frameworks.Twirp
3434
private import codeql.ruby.frameworks.Sqlite3
35+
private import codeql.ruby.frameworks.Sequel
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Provides modeling for `Sequel`, the database toolkit for Ruby.
3+
* https://github.com/jeremyevans/sequel
4+
*/
5+
6+
private import ruby
7+
private import codeql.ruby.ApiGraphs
8+
private import codeql.ruby.dataflow.FlowSummary
9+
private import codeql.ruby.Concepts
10+
11+
/**
12+
* Provides modeling for `Sequel`, the database toolkit for Ruby.
13+
* https://github.com/jeremyevans/sequel
14+
*/
15+
module Sequel {
16+
/** Flow Summary for `Sequel`. */
17+
private class SqlSummary extends SummarizedCallable {
18+
SqlSummary() { this = "Sequel.connect" }
19+
20+
override MethodCall getACall() { result = any(SequelConnection c).asExpr().getExpr() }
21+
22+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
23+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
24+
}
25+
}
26+
27+
/** A call to establish a connection to a database */
28+
private class SequelConnection extends DataFlow::CallNode {
29+
SequelConnection() {
30+
this =
31+
API::getTopLevelMember("Sequel").getAMethodCall(["connect", "sqlite", "mysql2", "jdbc"])
32+
}
33+
}
34+
35+
/** A call that constructs SQL statements */
36+
private class SequelConstruction extends SqlConstruction::Range, DataFlow::CallNode {
37+
DataFlow::Node query;
38+
39+
SequelConstruction() {
40+
this = API::getTopLevelMember("Sequel").getAMethodCall("cast") and query = this.getArgument(1)
41+
or
42+
this = API::getTopLevelMember("Sequel").getAMethodCall("function") and
43+
query = this.getArgument(0)
44+
}
45+
46+
override DataFlow::Node getSql() { result = query }
47+
}
48+
49+
/** A call that executes SQL statements against a database */
50+
private class SequelExecution extends SqlExecution::Range, DataFlow::CallNode {
51+
SequelExecution() {
52+
exists(SequelConnection sequelConnection |
53+
this =
54+
sequelConnection
55+
.getAMethodCall([
56+
"execute", "execute_ddl", "execute_dui", "execute_insert", "run", "<<", "fetch",
57+
"fetch_rows", "[]", "log_connection_yield"
58+
]) or
59+
this =
60+
sequelConnection
61+
.getAMethodCall("dataset")
62+
.getAMethodCall([
63+
"with_sql", "with_sql_all", "with_sql_delete", "with_sql_each", "with_sql_first",
64+
"with_sql_insert", "with_sql_single_value", "with_sql_update"
65+
])
66+
)
67+
}
68+
69+
override DataFlow::Node getSql() { result = this.getArgument(0) }
70+
}
71+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
sequelSqlConstruction
2+
| sequel.rb:63:29:63:49 | call to cast | sequel.rb:63:45:63:48 | name |
3+
| sequel.rb:66:29:66:49 | call to function | sequel.rb:66:45:66:48 | name |
4+
sequelSqlExecution
5+
| sequel.rb:10:9:10:60 | ...[...] | sequel.rb:10:14:10:59 | "SELECT * FROM users WHERE use..." |
6+
| sequel.rb:13:9:13:64 | call to run | sequel.rb:13:18:13:63 | "SELECT * FROM users WHERE use..." |
7+
| sequel.rb:16:9:18:11 | call to fetch | sequel.rb:16:20:16:65 | "SELECT * FROM users WHERE use..." |
8+
| sequel.rb:21:9:21:65 | ...[...] | sequel.rb:21:14:21:64 | "SELECT * FROM users WHERE use..." |
9+
| sequel.rb:24:9:24:65 | call to execute | sequel.rb:24:22:24:65 | "SELECT * FROM users WHERE use..." |
10+
| sequel.rb:27:9:27:71 | call to execute_ddl | sequel.rb:27:26:27:71 | "SELECT * FROM users WHERE use..." |
11+
| sequel.rb:30:9:30:71 | call to execute_dui | sequel.rb:30:26:30:71 | "SELECT * FROM users WHERE use..." |
12+
| sequel.rb:33:9:33:74 | call to execute_insert | sequel.rb:33:29:33:74 | "SELECT * FROM users WHERE use..." |
13+
| sequel.rb:36:9:36:62 | ... << ... | sequel.rb:36:17:36:62 | "SELECT * FROM users WHERE use..." |
14+
| sequel.rb:39:9:39:79 | call to fetch_rows | sequel.rb:39:25:39:70 | "SELECT * FROM users WHERE use..." |
15+
| sequel.rb:42:9:42:81 | call to with_sql_all | sequel.rb:42:35:42:80 | "SELECT * FROM users WHERE use..." |
16+
| sequel.rb:45:9:45:84 | call to with_sql_delete | sequel.rb:45:38:45:83 | "SELECT * FROM users WHERE use..." |
17+
| sequel.rb:48:9:48:90 | call to with_sql_each | sequel.rb:48:36:48:81 | "SELECT * FROM users WHERE use..." |
18+
| sequel.rb:51:9:51:83 | call to with_sql_first | sequel.rb:51:37:51:82 | "SELECT * FROM users WHERE use..." |
19+
| sequel.rb:54:9:54:84 | call to with_sql_insert | sequel.rb:54:38:54:83 | "SELECT * FROM users WHERE use..." |
20+
| sequel.rb:57:9:57:90 | call to with_sql_single_value | sequel.rb:57:44:57:89 | "SELECT * FROM users WHERE use..." |
21+
| sequel.rb:60:9:60:84 | call to with_sql_update | sequel.rb:60:38:60:83 | "SELECT * FROM users WHERE use..." |
22+
| sequel.rb:63:9:63:20 | ...[...] | sequel.rb:63:14:63:19 | :table |
23+
| sequel.rb:66:9:66:20 | ...[...] | sequel.rb:66:14:66:19 | :table |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
private import codeql.ruby.DataFlow
2+
private import codeql.ruby.Concepts
3+
private import codeql.ruby.frameworks.Sequel
4+
5+
query predicate sequelSqlConstruction(SqlConstruction c, DataFlow::Node sql) { sql = c.getSql() }
6+
7+
query predicate sequelSqlExecution(SqlExecution e, DataFlow::Node sql) { sql = e.getSql() }
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require 'sequel'
2+
3+
class UsersController < ActionController::Base
4+
def sequel_handler(event:, context:)
5+
name = params[:name]
6+
conn = Sequel.sqlite("sqlite://example.db")
7+
8+
# BAD: SQL statement constructed from user input
9+
conn["SELECT * FROM users WHERE username='#{name}'"]
10+
11+
# BAD: SQL statement constructed from user input
12+
conn.run("SELECT * FROM users WHERE username='#{name}'")
13+
14+
# BAD: SQL statement constructed from user input
15+
conn.fetch("SELECT * FROM users WHERE username='#{name}'") do |row|
16+
puts row[:name]
17+
end
18+
19+
# GOOD: SQL statement is not constructed from user input
20+
conn["SELECT * FROM users WHERE username='im_not_input'"]
21+
22+
# BAD: SQL statement constructed from user input
23+
conn.execute "SELECT * FROM users WHERE username=#{name}"
24+
25+
# BAD: SQL statement constructed from user input
26+
conn.execute_ddl "SELECT * FROM users WHERE username='#{name}'"
27+
28+
# BAD: SQL statement constructed from user input
29+
conn.execute_dui "SELECT * FROM users WHERE username='#{name}'"
30+
31+
# BAD: SQL statement constructed from user input
32+
conn.execute_insert "SELECT * FROM users WHERE username='#{name}'"
33+
34+
# BAD: SQL statement constructed from user input
35+
conn << "SELECT * FROM users WHERE username='#{name}'"
36+
37+
# BAD: SQL statement constructed from user input
38+
conn.fetch_rows("SELECT * FROM users WHERE username='#{name}'"){|row| }
39+
40+
# BAD: SQL statement constructed from user input
41+
conn.dataset.with_sql_all("SELECT * FROM users WHERE username='#{name}'")
42+
43+
# BAD: SQL statement constructed from user input
44+
conn.dataset.with_sql_delete("SELECT * FROM users WHERE username='#{name}'")
45+
46+
# BAD: SQL statement constructed from user input
47+
conn.dataset.with_sql_each("SELECT * FROM users WHERE username='#{name}'"){|row| }
48+
49+
# BAD: SQL statement constructed from user input
50+
conn.dataset.with_sql_first("SELECT * FROM users WHERE username='#{name}'")
51+
52+
# BAD: SQL statement constructed from user input
53+
conn.dataset.with_sql_insert("SELECT * FROM users WHERE username='#{name}'")
54+
55+
# BAD: SQL statement constructed from user input
56+
conn.dataset.with_sql_single_value("SELECT * FROM users WHERE username='#{name}'")
57+
58+
# BAD: SQL statement constructed from user input
59+
conn.dataset.with_sql_update("SELECT * FROM users WHERE username='#{name}'")
60+
61+
# BAD: SQL statement constructed from user input
62+
conn[:table].select(Sequel.cast(:a, name))
63+
64+
# BAD: SQL statement constructed from user input
65+
conn[:table].select(Sequel.function(name))
66+
end
67+
end

0 commit comments

Comments
 (0)