Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e6efccb

Browse files
jmthomasclaude
andcommitted
Use allowlist for tool config name validation instead of denylist
Replace denylist regex that blocked specific dangerous characters with an allowlist pattern that only permits letters, digits, hyphens, underscores, spaces, and periods. This is more secure as it rejects unexpected characters by default rather than trying to enumerate all dangerous ones. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent c0728f0 commit e6efccb

4 files changed

Lines changed: 48 additions & 30 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ 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 ..'
145+
if (!/^[A-Za-z0-9_\-. ]+$/.test(this.configName)) {
146+
return 'Config name must only contain letters, numbers, hyphens, underscores, spaces, and periods'
147147
}
148148
return null
149149
},

openc3/lib/openc3/models/tool_config_model.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,38 @@
1919

2020
module OpenC3
2121
class ToolConfigModel
22+
class InvalidNameError < StandardError; end
23+
24+
# Allowlist: letters, digits, hyphens, underscores, spaces, and periods
25+
VALID_NAME_PATTERN = /\A[A-Za-z0-9_\-. ]+\z/
26+
2227
def self.config_tool_names(scope: $openc3_scope)
2328
_, keys = Store.scan(0, match: "#{scope}__config__*", type: 'hash', count: 100)
2429
# Just return the tool name that is used in the other APIs
2530
return keys.map! { |key| key.split('__')[2] }.sort
2631
end
2732

2833
def self.list_configs(tool, scope: $openc3_scope)
29-
raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.})
34+
raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
3035
Store.hkeys("#{scope}__config__#{tool}")
3136
end
3237

3338
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{[/\\]|\.\.})
39+
raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
40+
raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
3641
Store.hget("#{scope}__config__#{tool}", name)
3742
end
3843

3944
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{[/\\]|\.\.})
45+
raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
46+
raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
4247
Store.hset("#{scope}__config__#{tool}", name, data)
4348
LocalMode.save_tool_config(scope, tool, name, data) if local_mode
4449
end
4550

4651
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{[/\\]|\.\.})
52+
raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
53+
raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
4954
Store.hdel("#{scope}__config__#{tool}", name)
5055
LocalMode.delete_tool_config(scope, tool, name) if local_mode
5156
end

openc3/python/openc3/models/tool_config_model.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from openc3.utilities.local_mode import LocalMode
1717
from openc3.utilities.store import Store
1818

19-
PATH_TRAVERSAL_PATTERN = re.compile(r"[/\\]|\.\.")
19+
20+
# Allowlist: letters, digits, hyphens, underscores, spaces, and periods
21+
VALID_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_\-. ]+$")
2022

2123

2224
class ToolConfigModel:
@@ -29,16 +31,16 @@ def config_tool_names(cls, scope: str = OPENC3_SCOPE):
2931

3032
@classmethod
3133
def list_configs(cls, tool: str, scope: str = OPENC3_SCOPE):
32-
if PATH_TRAVERSAL_PATTERN.search(tool):
34+
if not VALID_NAME_PATTERN.match(tool):
3335
raise RuntimeError(f"Invalid tool name: {tool}")
3436
keys = Store.hkeys(f"{scope}__config__{tool}")
3537
return [key.decode() for key in keys]
3638

3739
@classmethod
3840
def load_config(cls, tool: str, name: str, scope: str = OPENC3_SCOPE):
39-
if PATH_TRAVERSAL_PATTERN.search(tool):
41+
if not VALID_NAME_PATTERN.match(tool):
4042
raise RuntimeError(f"Invalid tool name: {tool}")
41-
if PATH_TRAVERSAL_PATTERN.search(name):
43+
if not VALID_NAME_PATTERN.match(name):
4244
raise RuntimeError(f"Invalid config name: {name}")
4345
return Store.hget(f"{scope}__config__{tool}", name).decode()
4446

@@ -51,19 +53,19 @@ def save_config(
5153
local_mode: bool = True,
5254
scope: str = OPENC3_SCOPE,
5355
):
54-
if PATH_TRAVERSAL_PATTERN.search(tool):
56+
if not VALID_NAME_PATTERN.match(tool):
5557
raise RuntimeError(f"Invalid tool name: {tool}")
56-
if PATH_TRAVERSAL_PATTERN.search(name):
58+
if not VALID_NAME_PATTERN.match(name):
5759
raise RuntimeError(f"Invalid config name: {name}")
5860
Store.hset(f"{scope}__config__{tool}", name, data)
5961
if local_mode:
6062
LocalMode.save_tool_config(scope, tool, name, data)
6163

6264
@classmethod
6365
def delete_config(cls, tool: str, name: str, local_mode: bool = True, scope: str = OPENC3_SCOPE):
64-
if PATH_TRAVERSAL_PATTERN.search(tool):
66+
if not VALID_NAME_PATTERN.match(tool):
6567
raise RuntimeError(f"Invalid tool name: {tool}")
66-
if PATH_TRAVERSAL_PATTERN.search(name):
68+
if not VALID_NAME_PATTERN.match(name):
6769
raise RuntimeError(f"Invalid config name: {name}")
6870
Store.hdel(f"{scope}__config__{tool}", name)
6971
if local_mode:

openc3/spec/models/tool_config_model_spec.rb

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,32 @@ module OpenC3
4444
expect(names[0]).to match(/.*\/DEFAULT\/tool_config\/toolie\/namely.json.*/)
4545
end
4646

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/)
47+
it "allows valid tool and config names" do
48+
ToolConfigModel.save_config('my-tool', 'My Config 1.0', '{}', local_mode: false, scope: 'DEFAULT')
49+
config = ToolConfigModel.load_config('my-tool', 'My Config 1.0', scope: 'DEFAULT')
50+
expect(config).to eq('{}')
5451
end
5552

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/)
53+
it "rejects invalid characters in tool name" do
54+
expect { ToolConfigModel.save_config('../evil', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
55+
expect { ToolConfigModel.save_config('evil/sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
56+
expect { ToolConfigModel.save_config('evil\\sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
57+
expect { ToolConfigModel.save_config('', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
58+
expect { ToolConfigModel.save_config('evil@name', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
59+
expect { ToolConfigModel.save_config('evil#name', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
60+
expect { ToolConfigModel.delete_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
61+
expect { ToolConfigModel.load_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
62+
expect { ToolConfigModel.list_configs('../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/)
63+
end
64+
65+
it "rejects invalid characters in config name" do
66+
expect { ToolConfigModel.save_config('tool', '../../etc/passwd', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
67+
expect { ToolConfigModel.save_config('tool', 'sub/dir', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
68+
expect { ToolConfigModel.save_config('tool', 'sub\\dir', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
69+
expect { ToolConfigModel.save_config('tool', '', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
70+
expect { ToolConfigModel.save_config('tool', 'name@evil', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
71+
expect { ToolConfigModel.delete_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
72+
expect { ToolConfigModel.load_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/)
6273
end
6374
end
6475
end

0 commit comments

Comments
 (0)