Skip to content

Commit afb820a

Browse files
authored
Merge pull request rsim#2558 from yahonda/harden-db-tasks-create-sql
Harden CREATE USER / GRANT SQL in DatabaseTasks#create
2 parents 81f5226 + 1eea3da commit afb820a

2 files changed

Lines changed: 93 additions & 3 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,44 @@ module ActiveRecord
77
module ConnectionAdapters
88
class OracleEnhancedAdapter
99
class DatabaseTasks < ActiveRecord::Tasks::AbstractTasks
10+
ORACLE_IDENTIFIER = /\A[[:alpha:]][\w$#]*\z/
11+
# GRANT operand: one or more space-separated tokens consisting of
12+
# word chars only. Covers system privileges (e.g. "create session",
13+
# "CREATE ANY TABLE") and simple role names. Rejects separators such
14+
# as `;`, `--`, `'`, `"`, as well as newlines/tabs — anything that
15+
# could turn a single GRANT into multiple statements or otherwise
16+
# make the operand hard to reason about.
17+
ORACLE_PRIVILEGE = /\A\w+( \w+)*\z/
18+
1019
def create
20+
username = configuration_hash[:username].to_s
21+
unless username.match?(ORACLE_IDENTIFIER)
22+
raise ArgumentError, "Invalid Oracle identifier for :username: #{username.inspect}"
23+
end
24+
OracleEnhancedAdapter.permissions.each do |permission|
25+
unless permission.to_s.match?(ORACLE_PRIVILEGE)
26+
raise ArgumentError, "Invalid Oracle privilege in OracleEnhancedAdapter.permissions: #{permission.inspect}"
27+
end
28+
end
29+
quoted_password = %("#{configuration_hash[:password].to_s.gsub('"', '""')}")
30+
1131
system_password = ENV.fetch("ORACLE_SYSTEM_PASSWORD") {
1232
print "Please provide the SYSTEM password for your Oracle installation (set ORACLE_SYSTEM_PASSWORD to avoid this prompt)\n>"
1333
$stdin.gets.strip
1434
}
1535
establish_connection(configuration_hash.merge(username: "SYSTEM", password: system_password))
1636
begin
17-
connection.execute "CREATE USER #{configuration_hash[:username]} IDENTIFIED BY #{configuration_hash[:password]}"
37+
connection.execute "CREATE USER #{username} IDENTIFIED BY #{quoted_password}"
1838
rescue => e
1939
if /ORA-01920/.match?(e.message) # user name conflicts with another user or role name
20-
connection.execute "ALTER USER #{configuration_hash[:username]} IDENTIFIED BY #{configuration_hash[:password]}"
40+
connection.execute "ALTER USER #{username} IDENTIFIED BY #{quoted_password}"
2141
else
2242
raise e
2343
end
2444
end
2545

2646
OracleEnhancedAdapter.permissions.each do |permission|
27-
connection.execute "GRANT #{permission} TO #{configuration_hash[:username]}"
47+
connection.execute "GRANT #{permission} TO #{username}"
2848
end
2949
end
3050

spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "active_record/connection_adapters/oracle_enhanced/database_tasks"
4+
require "active_support/testing/stream"
45
require "stringio"
56
require "tempfile"
67

@@ -65,6 +66,75 @@ def fake_terminal(input)
6566
end
6667
end
6768

69+
describe "create input validation" do
70+
around do |example|
71+
original_stderr, $stderr = $stderr, StringIO.new
72+
example.run
73+
ensure
74+
$stderr = original_stderr
75+
end
76+
77+
it "raises ArgumentError before touching the database when :username is not an Oracle identifier" do
78+
invalid_config = config.merge(username: "oracle;DROP USER system;--")
79+
expect(ActiveRecord::Base).not_to receive(:establish_connection)
80+
expect {
81+
ActiveRecord::Tasks::DatabaseTasks.create(invalid_config)
82+
}.to raise_error(ArgumentError, /Invalid Oracle identifier for :username/)
83+
end
84+
85+
it "raises ArgumentError before touching the database when OracleEnhancedAdapter.permissions contains a statement separator" do
86+
original_permissions = ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions
87+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions =
88+
["create session; DROP USER system;--"]
89+
expect(ActiveRecord::Base).not_to receive(:establish_connection)
90+
expect {
91+
ActiveRecord::Tasks::DatabaseTasks.create(config.merge(username: "oracle_enhanced_test_user"))
92+
}.to raise_error(ArgumentError, /Invalid Oracle privilege in OracleEnhancedAdapter.permissions/)
93+
ensure
94+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions = original_permissions
95+
end
96+
97+
it "raises ArgumentError before touching the database when OracleEnhancedAdapter.permissions contains a newline" do
98+
original_permissions = ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions
99+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions = ["create\nsession"]
100+
expect(ActiveRecord::Base).not_to receive(:establish_connection)
101+
expect {
102+
ActiveRecord::Tasks::DatabaseTasks.create(config.merge(username: "oracle_enhanced_test_user"))
103+
}.to raise_error(ArgumentError, /Invalid Oracle privilege in OracleEnhancedAdapter.permissions/)
104+
ensure
105+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions = original_permissions
106+
end
107+
end
108+
109+
describe "create password quoting" do
110+
include ActiveSupport::Testing::Stream
111+
112+
let(:captured_sqls) { [] }
113+
let(:fake_connection) do
114+
double("connection").tap do |c|
115+
allow(c).to receive(:execute) { |sql| captured_sqls << sql }
116+
end
117+
end
118+
119+
before do
120+
allow(ActiveRecord::Base).to receive(:establish_connection)
121+
allow(ActiveRecord::Base).to receive(:lease_connection).and_return(fake_connection)
122+
ENV["ORACLE_SYSTEM_PASSWORD"] = "dummy"
123+
end
124+
125+
after { ENV.delete("ORACLE_SYSTEM_PASSWORD") }
126+
127+
it "wraps :password as a double-quoted literal with embedded double quotes doubled" do
128+
quietly do
129+
ActiveRecord::Tasks::DatabaseTasks.create(
130+
config.merge(username: "oracle_enhanced_test_user", password: %{p"w})
131+
)
132+
end
133+
create_sql = captured_sqls.first
134+
expect(create_sql).to eq(%{CREATE USER oracle_enhanced_test_user IDENTIFIED BY "p""w"})
135+
end
136+
end
137+
68138
context "with test table" do
69139
before(:all) do
70140
$stdout, @original_stdout = StringIO.new, $stdout

0 commit comments

Comments
 (0)