VYPR
Medium severity4.3NVD Advisory· Published May 4, 2026· Updated May 8, 2026

CVE-2026-42085

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.

PackageAffected versionsPatched versions
openc3RubyGems
< 6.10.56.10.5
openc3RubyGems
>= 7.0.0.pre.rc1, < 7.0.0-rc37.0.0-rc3

Affected products

3
  • Openc3/Cosmos3 versions
    cpe:2.3:a:openc3:cosmos:7.0.0:rc1:*:*:open_source:*:*:*+ 2 more
    • cpe:2.3:a:openc3:cosmos:7.0.0:rc1:*:*:open_source:*:*:*
    • cpe:2.3:a:openc3:cosmos:7.0.0:rc2:*:*:open_source:*:*:*
    • cpe:2.3:a:openc3:cosmos:*:*:*:*:open_source:*:*:*range: <6.10.5

Patches

2
e6efccbd148b

Use allowlist for tool config name validation instead of denylist

https://github.com/OpenC3/cosmosJason ThomasMar 4, 2026via ghsa
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
    
9957a9fa460c

Prevent path traversal in tool config names

https://github.com/OpenC3/cosmosJason ThomasMar 3, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.