Skip to content

Commit a34e61a

Browse files
committed
Fix path traversal vulnerabilities
1 parent 762d7e0 commit a34e61a

File tree

8 files changed

+206
-91
lines changed

8 files changed

+206
-91
lines changed

openc3-cosmos-cmd-tlm-api/app/controllers/application_controller.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,34 @@ def authorization(permission, target_name: nil)
5757
end
5858
true
5959
end
60+
61+
def sanitize_params(param_list, require_params: true, allow_forward_slash: false)
62+
if require_params
63+
result = params.require(param_list)
64+
else
65+
result = []
66+
param_list.each do |param|
67+
result << params[param]
68+
end
69+
end
70+
result.each_with_index do |arg, index|
71+
if arg
72+
# Prevent the code scanner detects:
73+
# "Uncontrolled data used in path expression"
74+
# This method is taken directly from the Rails source:
75+
# https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
76+
if allow_forward_slash
77+
# Sometimes we have forward slashes so optionally allow those
78+
value = arg.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;\t\r\n\\", "-")
79+
else
80+
value = arg.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
81+
end
82+
if value != arg
83+
render(json: { status: 'error', message: "Invalid parameter #{param_list[index]}" }, status: 400)
84+
return false
85+
end
86+
end
87+
end
88+
return result
89+
end
6090
end

openc3-cosmos-cmd-tlm-api/app/controllers/plugins_controller.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,17 @@ def create(update = false)
3535
return unless authorization('admin')
3636
file = params[:plugin]
3737
if file
38+
scope = sanitize_params([:scope])
39+
return unless scope
40+
scope = scope[0]
3841
temp_dir = Dir.mktmpdir
3942
begin
4043
gem_file_path = temp_dir + '/' + file.original_filename
4144
FileUtils.cp(file.tempfile.path, gem_file_path)
4245
if @existing_model
43-
result = OpenC3::PluginModel.install_phase1(gem_file_path, existing_variables: @existing_model['variables'], existing_plugin_txt_lines: @existing_model['plugin_txt_lines'], scope: params[:scope])
46+
result = OpenC3::PluginModel.install_phase1(gem_file_path, existing_variables: @existing_model['variables'], existing_plugin_txt_lines: @existing_model['plugin_txt_lines'], scope: scope)
4447
else
45-
result = OpenC3::PluginModel.install_phase1(gem_file_path, scope: params[:scope])
48+
result = OpenC3::PluginModel.install_phase1(gem_file_path, scope: scope)
4649
end
4750
render :json => result
4851
rescue Exception => error
@@ -67,17 +70,22 @@ def update
6770
def install
6871
return unless authorization('admin')
6972
begin
73+
scope = sanitize_params([:scope])
74+
return unless scope
75+
scope = scope[0]
7076
temp_dir = Dir.mktmpdir
7177
plugin_hash_filename = Dir::Tmpname.create(['plugin-instance-', '.json']) {}
7278
plugin_hash_file_path = File.join(temp_dir, File.basename(plugin_hash_filename))
7379
File.open(plugin_hash_file_path, 'wb') do |file|
7480
file.write(params[:plugin_hash])
7581
end
7682

77-
gem_name = params[:id].split("__")[0]
83+
gem_name = sanitize_params([:id])
84+
return unless gem_name
85+
gem_name = gem_name[0].split("__")[0]
7886
result = OpenC3::ProcessManager.instance.spawn(
79-
["ruby", "/openc3/bin/openc3cli", "load", gem_name, params[:scope], plugin_hash_file_path, "force"], # force install
80-
"plugin_install", params[:id], Time.now + 1.hour, temp_dir: temp_dir, scope: params[:scope]
87+
["ruby", "/openc3/bin/openc3cli", "load", gem_name, scope, plugin_hash_file_path, "force"], # force install
88+
"plugin_install", params[:id], Time.now + 1.hour, temp_dir: temp_dir, scope: scope
8189
)
8290
render :json => result.name
8391
rescue Exception => error
@@ -88,7 +96,9 @@ def install
8896
def destroy
8997
return unless authorization('admin')
9098
begin
91-
result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "unload", params[:id], params[:scope]], "plugin_uninstall", params[:id], Time.now + 1.hour, scope: params[:scope])
99+
id, scope = sanitize_params([:id, scope])
100+
return unless id and scope
101+
result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "unload", id, scope], "plugin_uninstall", id, Time.now + 1.hour, scope: scope)
92102
render :json => result.name
93103
rescue Exception => error
94104
render(:json => { :status => 'error', :message => error.message }, :status => 500) and return

openc3-cosmos-cmd-tlm-api/app/controllers/screens_controller.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@
1919
class ScreensController < ApplicationController
2020
def index
2121
return unless authorization('system')
22-
render :json => Screen.all(*params.require([:scope]))
22+
scope = sanitize_params([:scope])
23+
return unless scope
24+
scope = scope[0]
25+
render :json => Screen.all(scope)
2326
end
2427

2528
def show
2629
return unless authorization('system')
27-
screen = Screen.find(*params.require([:scope, :target, :screen]))
30+
result = sanitize_params([:scope, :target, :screen])
31+
return unless result
32+
screen = Screen.find(*result)
2833
if screen
2934
render :json => screen
3035
else
@@ -34,7 +39,11 @@ def show
3439

3540
def create
3641
return unless authorization('system_set')
37-
screen = Screen.create(*params.require([:scope, :target, :screen, :text]))
42+
result = sanitize_params([:scope, :target, :screen])
43+
return unless result
44+
text = params.require([:text])[0]
45+
result << text
46+
screen = Screen.create(*result)
3847
OpenC3::Logger.info("Screen saved: #{params[:target]} #{params[:screen]}", scope: params[:scope], user: username())
3948
render :json => screen
4049
rescue => e
@@ -43,7 +52,9 @@ def create
4352

4453
def destroy
4554
return unless authorization('system_set')
46-
screen = Screen.destroy(*params.require([:scope, :target, :screen]))
55+
result = sanitize_params([:scope, :target, :screen])
56+
return unless result
57+
screen = Screen.destroy(*result)
4758
OpenC3::Logger.info("Screen deleted: #{params[:target]} #{params[:screen]}", scope: params[:scope], user: username())
4859
head :ok
4960
rescue => e

openc3-cosmos-cmd-tlm-api/app/controllers/storage_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
class StorageController < ApplicationController
2727
def buckets
28+
return unless authorization('system')
2829
# ENV.map returns a big array of mostly nils which is why we compact
2930
# The non-nil are MatchData objects due to the regex match
3031
matches = ENV.map { |key, _value| key.match(/^OPENC3_(.+)_BUCKET$/) }.compact
@@ -35,6 +36,7 @@ def buckets
3536
end
3637

3738
def volumes
39+
return unless authorization('system')
3840
# ENV.map returns a big array of mostly nils which is why we compact
3941
# The non-nil are MatchData objects due to the regex match
4042
matches = ENV.map { |key, _value| key.match(/^OPENC3_(.+)_VOLUME$/) }.compact

openc3-cosmos-cmd-tlm-api/app/controllers/tables_controller.rb

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@
2323
require 'base64'
2424

2525
class TablesController < ApplicationController
26-
before_action :sanitize_scope
27-
2826
def index
2927
return unless authorization('system')
30-
render json: Table.all(params[:scope])
28+
scope = sanitize_params([:scope])
29+
return unless scope
30+
scope = scope[0]
31+
render json: Table.all(scope)
3132
end
3233

3334
def binary
3435
return unless authorization('system')
36+
scope, binary, definition, table = sanitize_params([:scope, :binary, :definition, :table], require_params: false, allow_forward_slash: true)
37+
return unless scope
3538
begin
36-
file = Table.binary(params[:scope], params[:binary], params[:definition], params[:table])
39+
file = Table.binary(scope, binary, definition, table)
3740
results = { 'filename' => file.filename, 'contents' => Base64.encode64(file.contents) }
3841
render json: results
3942
rescue Table::NotFound => e
@@ -44,8 +47,10 @@ def binary
4447

4548
def definition
4649
return unless authorization('system')
50+
scope, definition, table = sanitize_params([:scope, :definition, :table], require_params: false, allow_forward_slash: true)
51+
return unless scope
4752
begin
48-
file = Table.definition(params[:scope], params[:definition], params[:table])
53+
file = Table.definition(scope, definition, table)
4954
render json: { 'filename' => file.filename, 'contents' => file.contents }
5055
rescue Table::NotFound => e
5156
render(json: { status: 'error', message: e.message }, status: 404) and
@@ -55,8 +60,10 @@ def definition
5560

5661
def report
5762
return unless authorization('system')
63+
scope, binary, definition, table = sanitize_params([:scope, :binary, :definition, :table], require_params: false, allow_forward_slash: true)
64+
return unless scope
5865
begin
59-
file = Table.report(params[:scope], params[:binary], params[:definition], params[:table])
66+
file = Table.report(scope, binary, definition, table)
6067
render json: { 'filename' => file.filename, 'contents' => file.contents }
6168
rescue Table::NotFound => e
6269
render(json: { status: 'error', message: e.message }, status: 404) and
@@ -66,17 +73,19 @@ def report
6673

6774
def body
6875
return unless authorization('system')
76+
scope, name = sanitize_params([:scope, :name], require_params: true, allow_forward_slash: true)
77+
return unless scope
6978
# body doesn't raise if not found ... it returns nil
70-
file = Table.body(params[:scope], params[:name])
79+
file = Table.body(scope, name)
7180
if file
7281
results = {}
7382

74-
if File.extname(params[:name]) == '.txt'
83+
if File.extname(name) == '.txt'
7584
results = { 'contents' => file }
7685
else
77-
locked = Table.locked?(params[:scope], params[:name])
86+
locked = Table.locked?(scope, name)
7887
unless locked
79-
Table.lock(params[:scope], params[:name], username())
88+
Table.lock(scope, name, username())
8089
end
8190
results = { 'contents' => Base64.encode64(file), 'locked' => locked }
8291
end
@@ -91,8 +100,10 @@ def body
91100

92101
def load
93102
return unless authorization('system')
103+
scope, binary, definition = sanitize_params([:scope, :binary, :definition], require_params: false, allow_forward_slash: true)
104+
return unless scope
94105
begin
95-
render json: Table.load(params[:scope], params[:binary], params[:definition])
106+
render json: Table.load(scope, binary, definition)
96107
rescue Table::NotFound => e
97108
render(json: { status: 'error', message: e.message }, status: 404) and
98109
return
@@ -101,8 +112,10 @@ def load
101112

102113
def save
103114
return unless authorization('system')
115+
scope, binary, definition = sanitize_params([:scope, :binary, :definition], require_params: false, allow_forward_slash: true)
116+
return unless scope
104117
begin
105-
Table.save(params[:scope], params[:binary], params[:definition], params[:tables])
118+
Table.save(scope, binary, definition, params[:tables])
106119
head :ok
107120
rescue Table::NotFound => e
108121
render(json: { status: 'error', message: e.message }, status: 404) and
@@ -112,8 +125,10 @@ def save
112125

113126
def save_as
114127
return unless authorization('system')
128+
scope, name, new_name = sanitize_params([:scope, :name, :new_name], require_params: true, allow_forward_slash: true)
129+
return unless scope
115130
begin
116-
Table.save_as(params[:scope], params[:name], params[:new_name])
131+
Table.save_as(scope, name, new_name)
117132
head :ok
118133
rescue Table::NotFound => e
119134
render(json: { status: 'error', message: e.message }, status: 404) and
@@ -123,8 +138,10 @@ def save_as
123138

124139
def generate
125140
return unless authorization('system')
141+
scope, definition = sanitize_params([:scope, :definition], require_params: false, allow_forward_slash: true)
142+
return unless scope
126143
begin
127-
filename = Table.generate(params[:scope], params[:definition])
144+
filename = Table.generate(scope, definition)
128145
render json: { 'filename' => filename }
129146
rescue Table::NotFound => e
130147
render(json: { status: 'error', message: e.message }, status: 404) and
@@ -134,40 +151,32 @@ def generate
134151

135152
def lock
136153
return unless authorization('system')
137-
Table.lock(params[:scope], params[:name], username())
154+
scope, name = sanitize_params([:scope, :name], require_params: true, allow_forward_slash: true)
155+
return unless scope
156+
Table.lock(scope, name, username())
138157
render status: 200
139158
end
140159

141160
def unlock
142161
return unless authorization('system')
143-
locked_by = Table.locked?(params[:scope], params[:name])
144-
Table.unlock(params[:scope], params[:name]) if username() == locked_by
162+
scope, name = sanitize_params([:scope, :name], require_params: true, allow_forward_slash: true)
163+
return unless scope
164+
locked_by = Table.locked?(scope, name)
165+
Table.unlock(scope, name) if username() == locked_by
145166
render status: 200
146167
end
147168

148169
def destroy
149170
return unless authorization('system')
171+
scope, name = sanitize_params([:scope, :name], require_params: true, allow_forward_slash: true)
172+
return unless scope
150173
# destroy returns no indication of success or failure so just assume it worked
151-
Table.destroy(params[:scope], params[:name])
174+
Table.destroy(scope, name)
152175
OpenC3::Logger.info(
153-
"Table destroyed: #{params[:name]}",
154-
scope: params[:scope],
176+
"Table destroyed: #{name}",
177+
scope: scope,
155178
user: username()
156179
)
157180
head :ok
158181
end
159-
160-
private
161-
162-
def sanitize_scope
163-
# scope is passed as a parameter and we use it to create paths in local_mode,
164-
# thus we have to sanitize it or the code scanner detects:
165-
# "Uncontrolled data used in path expression"
166-
# This method is taken directly from the Rails source:
167-
# https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
168-
scope = params[:scope].encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
169-
if scope != params[:scope]
170-
render(json: { status: 'error', message: "Invalid scope: #{params[:scope]}" }, status: 400)
171-
end
172-
end
173182
end

0 commit comments

Comments
 (0)