Skip to content

Commit 4eec005

Browse files
justin808claude
andcommitted
Fix shell injection vulnerability in server_manager.rb
Security improvements: - Replace string interpolation with env hash and argv array for Open3.capture3 - Add rails_env validation with allowlist pattern (/^[a-z0-9_]+$/i) - Update error handling to use safe command display - Add tests for rails_env validation and custom environment usage - Prevent arbitrary shell command execution via --rails-env parameter 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent de635c5 commit 4eec005

File tree

2 files changed

+42
-9
lines changed

2 files changed

+42
-9
lines changed

lib/react_on_rails/dev/server_manager.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,18 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
268268
)
269269

270270
# Precompile assets with production webpack optimizations (includes pack generation automatically)
271-
env_vars = ["NODE_ENV=production"]
272-
env_vars << "RAILS_ENV=#{rails_env}" if rails_env
273-
command = "#{env_vars.join(' ')} bundle exec rails assets:precompile"
271+
env = { "NODE_ENV" => "production" }
272+
273+
# Validate and sanitize rails_env to prevent shell injection
274+
if rails_env
275+
unless rails_env.match?(/\A[a-z0-9_]+\z/i)
276+
puts "❌ Invalid rails_env: '#{rails_env}'. Must contain only letters, numbers, and underscores."
277+
exit 1
278+
end
279+
env["RAILS_ENV"] = rails_env
280+
end
281+
282+
argv = ["bundle", "exec", "rails", "assets:precompile"]
274283

275284
puts "🔨 Precompiling assets with production webpack optimizations..."
276285
puts ""
@@ -288,12 +297,14 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
288297
puts " • Gets production webpack bundles without production Rails complexity"
289298
end
290299
puts ""
291-
puts "#{Rainbow('💻 Running:').blue} #{command}"
300+
301+
env_display = env.map { |k, v| "#{k}=#{v}" }.join(" ")
302+
puts "#{Rainbow('💻 Running:').blue} #{env_display} #{argv.join(' ')}"
292303
puts ""
293304

294305
# Capture both stdout and stderr
295306
require "open3"
296-
stdout, stderr, status = Open3.capture3(command)
307+
stdout, stderr, status = Open3.capture3(env, *argv)
297308

298309
if status.success?
299310
puts "✅ Assets precompiled successfully"
@@ -317,11 +328,12 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
317328
end
318329

319330
puts Rainbow("🛠️ To debug this issue:").yellow.bold
331+
command_display = "#{env_display} #{argv.join(' ')}"
320332
puts "#{Rainbow('1.').cyan} #{Rainbow('Run the command separately to see detailed output:').white}"
321-
puts " #{Rainbow(command).cyan}"
333+
puts " #{Rainbow(command_display).cyan}"
322334
puts ""
323335
puts "#{Rainbow('2.').cyan} #{Rainbow('Add --trace for full stack trace:').white}"
324-
puts " #{Rainbow("#{command} --trace").cyan}"
336+
puts " #{Rainbow("#{command_display} --trace").cyan}"
325337
puts ""
326338
puts "#{Rainbow('3.').cyan} #{Rainbow('Or try with development webpack (faster, less optimized):').white}"
327339
puts " #{Rainbow('NODE_ENV=development bundle exec rails assets:precompile').cyan}"

spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,36 @@ def mock_system_calls
5454
end
5555

5656
it "starts production-like mode" do
57-
command = "NODE_ENV=production bundle exec rails assets:precompile"
57+
env = { "NODE_ENV" => "production" }
58+
argv = ["bundle", "exec", "rails", "assets:precompile"]
5859
status_double = instance_double(Process::Status, success?: true)
59-
expect(Open3).to receive(:capture3).with(command).and_return(["output", "", status_double])
60+
expect(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double])
6061
expect(ReactOnRails::Dev::ProcessManager).to receive(:ensure_procfile).with("Procfile.dev-prod-assets")
6162
expect(ReactOnRails::Dev::ProcessManager).to receive(:run_with_process_manager).with("Procfile.dev-prod-assets")
6263

6364
described_class.start(:production_like)
6465
end
6566

67+
it "starts production-like mode with custom rails_env" do
68+
env = { "NODE_ENV" => "production", "RAILS_ENV" => "staging" }
69+
argv = ["bundle", "exec", "rails", "assets:precompile"]
70+
status_double = instance_double(Process::Status, success?: true)
71+
expect(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double])
72+
expect(ReactOnRails::Dev::ProcessManager).to receive(:ensure_procfile).with("Procfile.dev-prod-assets")
73+
expect(ReactOnRails::Dev::ProcessManager).to receive(:run_with_process_manager).with("Procfile.dev-prod-assets")
74+
75+
described_class.start(:production_like, nil, verbose: false, rails_env: "staging")
76+
end
77+
78+
it "rejects invalid rails_env with shell injection characters" do
79+
expect_any_instance_of(Kernel).to receive(:exit).with(1)
80+
allow_any_instance_of(Kernel).to receive(:puts) # Allow other puts calls
81+
error_pattern = /Invalid rails_env.*Must contain only letters, numbers, and underscores/
82+
expect_any_instance_of(Kernel).to receive(:puts).with(error_pattern)
83+
84+
described_class.start(:production_like, nil, verbose: false, rails_env: "production; rm -rf /")
85+
end
86+
6687
it "raises error for unknown mode" do
6788
expect { described_class.start(:unknown) }.to raise_error(ArgumentError, "Unknown mode: unknown")
6889
end

0 commit comments

Comments
 (0)