CVE-2026-42085
Description
OpenC3 COSMOS provides the functionality needed to send commands to and receive data from one or more embedded systems. Prior to versions 6.10.5 and 7.0.0-rc3, OpenC3 COSMOS contains a design flaw in the save_tool_config() function that allows saving tool configuration files at arbitrary locations inside the shared /plugins directory tree by supplying crafted configuration filenames. Although the implementation sufficiently mitigates standard path traversal attacks, by canonicalizing filename to an absolute path, all plugins share this same root directory. That enables users to create arbitrary file structures and overwrite existing configuration files within the shared /plugins directory. This issue has been patched in versions 6.10.5 and 7.0.0-rc3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openc3RubyGems | < 6.10.5 | 6.10.5 |
openc3RubyGems | >= 7.0.0.pre.rc1, < 7.0.0-rc3 | 7.0.0-rc3 |
Affected products
3Patches
2e6efccbd148bUse allowlist for tool config name validation instead of denylist
4 files changed · +48 −30
openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/config/SaveConfigDialog.vue+2 −2 modified@@ -142,8 +142,8 @@ export default { if (!this.configName) { return 'Config must have a name' } - if (/[/\\]|\.\./.test(this.configName)) { - return 'Config name must not contain / \\ or ..' + if (!/^[A-Za-z0-9_\-. ]+$/.test(this.configName)) { + return 'Config name must only contain letters, numbers, hyphens, underscores, spaces, and periods' } return null },
openc3/lib/openc3/models/tool_config_model.rb+12 −7 modified@@ -19,33 +19,38 @@ module OpenC3 class ToolConfigModel + class InvalidNameError < StandardError; end + + # Allowlist: letters, digits, hyphens, underscores, spaces, and periods + VALID_NAME_PATTERN = /\A[A-Za-z0-9_\-. ]+\z/ + def self.config_tool_names(scope: $openc3_scope) _, keys = Store.scan(0, match: "#{scope}__config__*", type: 'hash', count: 100) # Just return the tool name that is used in the other APIs return keys.map! { |key| key.split('__')[2] }.sort end def self.list_configs(tool, scope: $openc3_scope) - raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) + raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN) Store.hkeys("#{scope}__config__#{tool}") end def self.load_config(tool, name, scope: $openc3_scope) - raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) - raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) + raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN) + raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN) Store.hget("#{scope}__config__#{tool}", name) end def self.save_config(tool, name, data, local_mode: true, scope: $openc3_scope) - raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) - raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) + raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN) + raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN) Store.hset("#{scope}__config__#{tool}", name, data) LocalMode.save_tool_config(scope, tool, name, data) if local_mode end def self.delete_config(tool, name, local_mode: true, scope: $openc3_scope) - raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) - raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) + raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN) + raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN) Store.hdel("#{scope}__config__#{tool}", name) LocalMode.delete_tool_config(scope, tool, name) if local_mode end
openc3/python/openc3/models/tool_config_model.py+10 −8 modified@@ -16,7 +16,9 @@ from openc3.utilities.local_mode import LocalMode from openc3.utilities.store import Store -PATH_TRAVERSAL_PATTERN = re.compile(r"[/\\]|\.\.") + +# Allowlist: letters, digits, hyphens, underscores, spaces, and periods +VALID_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_\-. ]+$") class ToolConfigModel: @@ -29,16 +31,16 @@ def config_tool_names(cls, scope: str = OPENC3_SCOPE): @classmethod def list_configs(cls, tool: str, scope: str = OPENC3_SCOPE): - if PATH_TRAVERSAL_PATTERN.search(tool): + if not VALID_NAME_PATTERN.match(tool): raise RuntimeError(f"Invalid tool name: {tool}") keys = Store.hkeys(f"{scope}__config__{tool}") return [key.decode() for key in keys] @classmethod def load_config(cls, tool: str, name: str, scope: str = OPENC3_SCOPE): - if PATH_TRAVERSAL_PATTERN.search(tool): + if not VALID_NAME_PATTERN.match(tool): raise RuntimeError(f"Invalid tool name: {tool}") - if PATH_TRAVERSAL_PATTERN.search(name): + if not VALID_NAME_PATTERN.match(name): raise RuntimeError(f"Invalid config name: {name}") return Store.hget(f"{scope}__config__{tool}", name).decode() @@ -51,19 +53,19 @@ def save_config( local_mode: bool = True, scope: str = OPENC3_SCOPE, ): - if PATH_TRAVERSAL_PATTERN.search(tool): + if not VALID_NAME_PATTERN.match(tool): raise RuntimeError(f"Invalid tool name: {tool}") - if PATH_TRAVERSAL_PATTERN.search(name): + if not VALID_NAME_PATTERN.match(name): raise RuntimeError(f"Invalid config name: {name}") Store.hset(f"{scope}__config__{tool}", name, data) if local_mode: LocalMode.save_tool_config(scope, tool, name, data) @classmethod def delete_config(cls, tool: str, name: str, local_mode: bool = True, scope: str = OPENC3_SCOPE): - if PATH_TRAVERSAL_PATTERN.search(tool): + if not VALID_NAME_PATTERN.match(tool): raise RuntimeError(f"Invalid tool name: {tool}") - if PATH_TRAVERSAL_PATTERN.search(name): + if not VALID_NAME_PATTERN.match(name): raise RuntimeError(f"Invalid config name: {name}") Store.hdel(f"{scope}__config__{tool}", name) if local_mode:
openc3/spec/models/tool_config_model_spec.rb+24 −13 modified@@ -44,21 +44,32 @@ module OpenC3 expect(names[0]).to match(/.*\/DEFAULT\/tool_config\/toolie\/namely.json.*/) end - it "rejects path traversal in tool name" do - expect { ToolConfigModel.save_config('../evil', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) - expect { ToolConfigModel.save_config('evil/sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) - expect { ToolConfigModel.save_config('evil\\sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) - expect { ToolConfigModel.delete_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) - expect { ToolConfigModel.load_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) - expect { ToolConfigModel.list_configs('../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + it "allows valid tool and config names" do + ToolConfigModel.save_config('my-tool', 'My Config 1.0', '{}', local_mode: false, scope: 'DEFAULT') + config = ToolConfigModel.load_config('my-tool', 'My Config 1.0', scope: 'DEFAULT') + expect(config).to eq('{}') end - it "rejects path traversal in config name" do - expect { ToolConfigModel.save_config('tool', '../../etc/passwd', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) - expect { ToolConfigModel.save_config('tool', 'sub/dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) - expect { ToolConfigModel.save_config('tool', 'sub\\dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) - expect { ToolConfigModel.delete_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) - expect { ToolConfigModel.load_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + it "rejects invalid characters in tool name" do + expect { ToolConfigModel.save_config('../evil', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil/sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil\\sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.save_config('', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil@name', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil#name', 'name', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.delete_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.load_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + expect { ToolConfigModel.list_configs('../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid tool name/) + end + + it "rejects invalid characters in config name" do + expect { ToolConfigModel.save_config('tool', '../../etc/passwd', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', 'sub/dir', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', 'sub\\dir', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', '', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', 'name@evil', '{}', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.delete_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) + expect { ToolConfigModel.load_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(ToolConfigModel::InvalidNameError, /Invalid config name/) end end end
9957a9fa460cPrevent path traversal in tool config names
4 files changed · +45 −1
openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/config/SaveConfigDialog.vue+4 −1 modified@@ -8,7 +8,7 @@ # See LICENSE.md for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2026, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -142,6 +142,9 @@ export default { if (!this.configName) { return 'Config must have a name' } + if (/[/\\]|\.\./.test(this.configName)) { + return 'Config name must not contain / \\ or ..' + } return null }, show: {
openc3/lib/openc3/models/tool_config_model.rb+7 −0 modified@@ -26,19 +26,26 @@ def self.config_tool_names(scope: $openc3_scope) end def self.list_configs(tool, scope: $openc3_scope) + raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) Store.hkeys("#{scope}__config__#{tool}") end def self.load_config(tool, name, scope: $openc3_scope) + raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) + raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) Store.hget("#{scope}__config__#{tool}", name) end def self.save_config(tool, name, data, local_mode: true, scope: $openc3_scope) + raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) + raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) Store.hset("#{scope}__config__#{tool}", name, data) LocalMode.save_tool_config(scope, tool, name, data) if local_mode end def self.delete_config(tool, name, local_mode: true, scope: $openc3_scope) + raise "Invalid tool name: #{tool}" if tool.match?(%r{[/\\]|\.\.}) + raise "Invalid config name: #{name}" if name.match?(%r{[/\\]|\.\.}) Store.hdel("#{scope}__config__#{tool}", name) LocalMode.delete_tool_config(scope, tool, name) if local_mode end
openc3/python/openc3/models/tool_config_model.py+17 −0 modified@@ -9,12 +9,15 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +import re from typing import Any from openc3.environment import OPENC3_SCOPE from openc3.utilities.local_mode import LocalMode from openc3.utilities.store import Store +PATH_TRAVERSAL_PATTERN = re.compile(r"[/\\]|\.\.") + class ToolConfigModel: @classmethod @@ -26,11 +29,17 @@ def config_tool_names(cls, scope: str = OPENC3_SCOPE): @classmethod def list_configs(cls, tool: str, scope: str = OPENC3_SCOPE): + if PATH_TRAVERSAL_PATTERN.search(tool): + raise RuntimeError(f"Invalid tool name: {tool}") keys = Store.hkeys(f"{scope}__config__{tool}") return [key.decode() for key in keys] @classmethod def load_config(cls, tool: str, name: str, scope: str = OPENC3_SCOPE): + if PATH_TRAVERSAL_PATTERN.search(tool): + raise RuntimeError(f"Invalid tool name: {tool}") + if PATH_TRAVERSAL_PATTERN.search(name): + raise RuntimeError(f"Invalid config name: {name}") return Store.hget(f"{scope}__config__{tool}", name).decode() @classmethod @@ -42,12 +51,20 @@ def save_config( local_mode: bool = True, scope: str = OPENC3_SCOPE, ): + if PATH_TRAVERSAL_PATTERN.search(tool): + raise RuntimeError(f"Invalid tool name: {tool}") + if PATH_TRAVERSAL_PATTERN.search(name): + raise RuntimeError(f"Invalid config name: {name}") Store.hset(f"{scope}__config__{tool}", name, data) if local_mode: LocalMode.save_tool_config(scope, tool, name, data) @classmethod def delete_config(cls, tool: str, name: str, local_mode: bool = True, scope: str = OPENC3_SCOPE): + if PATH_TRAVERSAL_PATTERN.search(tool): + raise RuntimeError(f"Invalid tool name: {tool}") + if PATH_TRAVERSAL_PATTERN.search(name): + raise RuntimeError(f"Invalid config name: {name}") Store.hdel(f"{scope}__config__{tool}", name) if local_mode: LocalMode.delete_tool_config(scope, tool, name)
openc3/spec/models/tool_config_model_spec.rb+17 −0 modified@@ -43,6 +43,23 @@ module OpenC3 names = ToolConfigModel.delete_config('toolie', 'namely', local_mode: true, scope: 'DEFAULT') expect(names[0]).to match(/.*\/DEFAULT\/tool_config\/toolie\/namely.json.*/) end + + it "rejects path traversal in tool name" do + expect { ToolConfigModel.save_config('../evil', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil/sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + expect { ToolConfigModel.save_config('evil\\sub', 'name', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + expect { ToolConfigModel.delete_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + expect { ToolConfigModel.load_config('../evil', 'name', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + expect { ToolConfigModel.list_configs('../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid tool name/) + end + + it "rejects path traversal in config name" do + expect { ToolConfigModel.save_config('tool', '../../etc/passwd', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', 'sub/dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + expect { ToolConfigModel.save_config('tool', 'sub\\dir', '{}', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + expect { ToolConfigModel.delete_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + expect { ToolConfigModel.load_config('tool', '../evil', scope: 'DEFAULT') }.to raise_error(RuntimeError, /Invalid config name/) + end end end end
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/OpenC3/cosmos/commit/9957a9fa460c0c0cf5cdbf6a5931bbdd025246a5nvdPatchWEB
- github.com/OpenC3/cosmos/commit/e6efccbd148ba0e3361c5891027f2373aa140d42nvdPatchWEB
- github.com/OpenC3/cosmos/security/advisories/GHSA-4jvx-93h3-f45hnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-4jvx-93h3-f45hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-42085ghsaADVISORY
- github.com/OpenC3/cosmos/releases/tag/v6.10.5nvdRelease NotesWEB
- github.com/OpenC3/cosmos/releases/tag/v7.0.0-rc3nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.