Skip to content

Commit 9957a9f

Browse files
jmthomasclaude
andcommitted
Prevent path traversal in tool config names
Reject tool and config names containing /, \, or .. in ToolConfigModel to prevent writing arbitrary files within the shared /plugins directory. Added client-side validation in SaveConfigDialog.vue and backend validation in both Ruby and Python models with corresponding specs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 67fb840 commit 9957a9f

4 files changed

Lines changed: 45 additions & 1 deletion

File tree

openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/config/SaveConfigDialog.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# See LICENSE.md for more details.
99
1010
# Modified by OpenC3, Inc.
11-
# All changes Copyright 2022, OpenC3, Inc.
11+
# All changes Copyright 2026, OpenC3, Inc.
1212
# All Rights Reserved
1313
#
1414
# This file may also be used under the terms of a commercial license
@@ -142,6 +142,9 @@ export default {
142142
if (!this.configName) {
143143
return 'Config must have a name'
144144
}
145+
if (/[/\\]|\.\./.test(this.configName)) {
146+
return 'Config name must not contain / \\ or ..'
147+
}
145148
return null
146149
},
147150
show: {

openc3/lib/openc3/models/tool_config_model.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,26 @@ def self.config_tool_names(scope: $openc3_scope)
2626
end
2727

2828
def self.list_configs(tool, scope: $openc3_scope)
29+
raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.})
2930
Store.hkeys("#{scope}__config__#{tool}")
3031
end
3132

3233
def self.load_config(tool, name, scope: $openc3_scope)
34+
raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.})
35+
raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.})
3336
Store.hget("#{scope}__config__#{tool}", name)
3437
end
3538

3639
def self.save_config(tool, name, data, local_mode: true, scope: $openc3_scope)
40+
raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.})
41+
raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.})
3742
Store.hset("#{scope}__config__#{tool}", name, data)
3843
LocalMode.save_tool_config(scope, tool, name, data) if local_mode
3944
end
4045

4146
def self.delete_config(tool, name, local_mode: true, scope: $openc3_scope)
47+
raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.})
48+
raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.})
4249
Store.hdel("#{scope}__config__#{tool}", name)
4350
LocalMode.delete_tool_config(scope, tool, name) if local_mode
4451
end

openc3/python/openc3/models/tool_config_model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
# This file may also be used under the terms of a commercial license
1010
# if purchased from OpenC3, Inc.
1111

12+
import re
1213
from typing import Any
1314

1415
from openc3.environment import OPENC3_SCOPE
1516
from openc3.utilities.local_mode import LocalMode
1617
from openc3.utilities.store import Store
1718

19+
PATH_TRAVERSAL_PATTERN = re.compile(r"[/\\]|\.\.")
20+
1821

1922
class ToolConfigModel:
2023
@classmethod
@@ -26,11 +29,17 @@ def config_tool_names(cls, scope: str = OPENC3_SCOPE):
2629

2730
@classmethod
2831
def list_configs(cls, tool: str, scope: str = OPENC3_SCOPE):
32+
if PATH_TRAVERSAL_PATTERN.search(tool):
33+
raise RuntimeError(f"Invalid tool name: {tool}")
2934
keys = Store.hkeys(f"{scope}__config__{tool}")
3035
return [key.decode() for key in keys]
3136

3237
@classmethod
3338
def load_config(cls, tool: str, name: str, scope: str = OPENC3_SCOPE):
39+
if PATH_TRAVERSAL_PATTERN.search(tool):
40+
raise RuntimeError(f"Invalid tool name: {tool}")
41+
if PATH_TRAVERSAL_PATTERN.search(name):
42+
raise RuntimeError(f"Invalid config name: {name}")
3443
return Store.hget(f"{scope}__config__{tool}", name).decode()
3544

3645
@classmethod
@@ -42,12 +51,20 @@ def save_config(
4251
local_mode: bool = True,
4352
scope: str = OPENC3_SCOPE,
4453
):
54+
if PATH_TRAVERSAL_PATTERN.search(tool):
55+
raise RuntimeError(f"Invalid tool name: {tool}")
56+
if PATH_TRAVERSAL_PATTERN.search(name):
57+
raise RuntimeError(f"Invalid config name: {name}")
4558
Store.hset(f"{scope}__config__{tool}", name, data)
4659
if local_mode:
4760
LocalMode.save_tool_config(scope, tool, name, data)
4861

4962
@classmethod
5063
def delete_config(cls, tool: str, name: str, local_mode: bool = True, scope: str = OPENC3_SCOPE):
64+
if PATH_TRAVERSAL_PATTERN.search(tool):
65+
raise RuntimeError(f"Invalid tool name: {tool}")
66+
if PATH_TRAVERSAL_PATTERN.search(name):
67+
raise RuntimeError(f"Invalid config name: {name}")
5168
Store.hdel(f"{scope}__config__{tool}", name)
5269
if local_mode:
5370
LocalMode.delete_tool_config(scope, tool, name)

openc3/spec/models/tool_config_model_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ module OpenC3
4343
names = ToolConfigModel.delete_config('toolie', 'namely', local_mode: true, scope: 'DEFAULT')
4444
expect(names[0]).to match(/.*\/DEFAULT\/tool_config\/toolie\/namely.json.*/)
4545
end
46+
47+
it "rejects path traversal in tool name" do
48+
expect { ToolConfigModel.save_config('../evil', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
49+
expect { ToolConfigModel.save_config('evil/sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
50+
expect { ToolConfigModel.save_config('evil\\sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
51+
expect { ToolConfigModel.delete_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
52+
expect { ToolConfigModel.load_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
53+
expect { ToolConfigModel.list_configs('../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/)
54+
end
55+
56+
it "rejects path traversal in config name" do
57+
expect { ToolConfigModel.save_config('tool', '../../etc/passwd', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/)
58+
expect { ToolConfigModel.save_config('tool', 'sub/dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/)
59+
expect { ToolConfigModel.save_config('tool', 'sub\\dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/)
60+
expect { ToolConfigModel.delete_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/)
61+
expect { ToolConfigModel.load_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/)
62+
end
4663
end
4764
end
4865
end

0 commit comments

Comments
 (0)