GitHub Advisoryadvisory · RubyGems
GHSA-jjq7-m736-w977 OpenC3 COSMOS: Authenticated remote code execution via the user-writable config overlay (table definitions, cmd/tlm definitions, and script suites)
CRITICAL
Official page ↗
- Aliases
- CVE-2026-77602
- Ecosystem
- RubyGems
- Packages
- openc3
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-09-23
Affected packages
• RubyGems openc3: >= 5.1.0, < 7.3.0 → fixed in 7.3.0
Description
Summary
COSMOS reads configuration from a user-writable overlay (`targets_modified/`) before the read-only plugin-installed `targets/` tree, and the config subsystem executes code on those files: `ConfigParser` renders every file as ERB by default, a `GENERIC_READ_CONVERSION` / `GENERIC_WRITE_CONVERSION` block is evaluated as code by `GenericConversion` (Ruby and Python), and the Script Runner suite analysis `require`s a procedure file. An authenticated user can write into `targets_modified/` below the admin tier (the storage-upload endpoint exempts that area from the admin gate, and the screen-save endpoint stores its body verbatim there), so the same root cause is reachable through several features, each giving arbitrary code execution on a COSMOS server.
Three vulnerable routes were identified, all reachable by an authenticated non-admin user (in the open-source edition `authorize` ignores the permission string, so any authenticated user qualifies):
1. Table definitions (immediate). `tables#generate|report|load` reads a definition from `targets_modified/` and ERB-renders it and evaluates its `GENERIC_*_CONVERSION` block in the `cmd-tlm-api` container.
2. Command/telemetry definitions (persistent). A file written to `targets_modified//cmd_tlm/` is overlaid by `System.setup_targets` and processed by `PacketConfig` in the decom/multi microservices: ERB-rendered in the Ruby implementation, and GENERIC-evaluated in both the Ruby and Python implementations (the Python `ConfigParser` does not run ERB). It executes on the next microservice (re)start.
3. Script Runner suites (immediate). A procedure written to `targets_modified//procedures/` is `require`d by the suite analysis, reachable at the read-only `script_view` tier through `scripts#body` and `running_script#show` (the analysis subprocess is spawned when `OPENC3_SERVICE_PASSWORD` is configured, which it is in the shipped `.env`).
Details
Root cause. `TargetFile.body` (Ruby `openc3/lib/openc3/utilities/target_file.rb`, Python `openc3/python/openc3/utilities/target_file.py`) reads `{scope}/targets_modified/{name}` before `{scope}/targets/{name}`. The storage-upload endpoint `storage_controller.rb get_upload_presigned_request` is gated at `system_set` and exempts `targets_modified/` and `tmp/` from its `admin` check (so a write there is not admin-gated); for a target path it additionally calls `authorize_bucket_path`, which in the permission-enforcing edition requires `tlm` on the target, while in the open-source edition `authorize` ignores the permission string entirely. `screens_controller.rb create` (`system_set`) stores its request body verbatim under `targets_modified//screens/`. So a non-admin can place files in the overlay. The config subsystem then executes them.
Sink 1, ERB. `ConfigParser#parse_file` renders the file as ERB before parsing (`run_erb` defaults to true):
```ruby
openc3/lib/openc3/config/config_parser.rb:402
output = ERB.new(File.read(filename)...comment_erb(), trim_mode: "-").result(...)
```
Reached for table definitions via `tables_controller.rb` -> `Table.get_definitions` -> `TableConfig.process_file` -> `parse_file`, and for cmd/tlm definitions via `System.setup_targets` (`system.rb`, whose overlay loop copies `targets_modified//cmd_tlm/*` over the read-only files) -> `PacketConfig#process_file` -> `parse_file`.
Sink 2, GENERIC conversion. `PacketConfig`/`TableConfig` build a `GenericConversion` from a `GENERIC_READ_CONVERSION_START .. END` / `GENERIC_WRITE_CONVERSION_START .. END` block, and `GenericConversion#call` evaluates it (independent of ERB):
```ruby
openc3/lib/openc3/conversions/generic_conversion.rb (call)
eval(@code_to_eval)
Python openc3/python/openc3/conversions/generic_conversion.py (call): compile()/exec()/eval()
```
The read conversion fires on `tables#report`/`load` and during telemetry decom; the write conversion fires on `tables#generate` and on `restore_defaults`.
Sink 3, suite require. The Script Runner suite analysis executes the file:
```ruby
openc3-cosmos-script-runner-api/scripts/run_suite_analysis.rb:24
require ARGV[1] # runs all top-level code of the supplied file
```
reached from `Script.process_suite`, invoked by `scripts#body` and `running_script#show` (both `script_view`) and `scripts#create` (`script_edit`) when the file matches the suite pattern.
Permission tiers. Writing the payload needs `system_set` (screen save, storage upload) or `script_edit` (script create); triggering needs `system` (tables) or `script_view` (suite). These are below the tiers where COSMOS gates code execution elsewhere (plugin install requires `admin`, running a script requires `script_run`).
PoC
Table definition path, against a standard stack. Benign payload writes `id` to a marker file. This PoC uses the open-source password login; in the permission-enforcing edition substitute a bearer token for a user holding the permissions noted above.
```bash
BASE=http://localhost:2900/openc3-api # adjust to your deployment
TOKEN=$(curl -s -X POST "$BASE/auth/verify" -H 'Content-Type: application/json' -d '{"password":""}')
1) Write the payload into targets_modified/ via the screen save endpoint.
curl -s -X POST "$BASE/screen" -H "Authorization: $TOKEN" \
--data-urlencode 'scope=DEFAULT' --data-urlencode 'target=INST' --data-urlencode 'screen=poc' \
--data-urlencode $'text=SCREEN AUTO AUTO 1.0\n\nLABEL poc'
2) Trigger by pointing a table action at that file.
curl -s -X POST "$BASE/tables/generate" -H "Authorization: $TOKEN" \
--data-urlencode 'scope=DEFAULT' --data-urlencode 'definition=INST/screens/poc.txt'
```
Then in the cmd-tlm-api container: `cat /tmp/erb_rce_poc` shows `uid=1001(openc3) ...`. The `tables/generate` request returns HTTP 500 (the screen lines are not valid table keywords); the marker shows the code already ran.
The same outcome without ERB, using the GENERIC sink, on the same `tables/generate` trigger:
```
TABLE "data" BIG_ENDIAN KEY_VALUE "poc"
APPEND_PARAMETER "item1" 8 UINT 0 255 0 "Item"
GENERIC_WRITE_CONVERSION_START
`id > /tmp/erb_rce_poc`
0
GENERIC_WRITE_CONVERSION_END
```
cmd/tlm path: upload a telemetry definition containing the same ERB or GENERIC block to `targets_modified//cmd_tlm/.txt` via the storage-upload presigned request (`system_set`), then the code runs in that target's decom microservice on its next restart. Suite path: write a suite-shaped procedure to `targets_modified//procedures/.rb` and call `scripts#body` on it at `script_view`.
The ERB table chain was confirmed end to end over HTTP against a booted Rails and puma instance.
Impact
Arbitrary code execution as the `openc3` user in the `cmd-tlm-api` container and the per-target decom microservices and the script-runner. Those processes hold the Redis and bucket credentials and sit on the internal service network, so the executed code acts with that authority over configuration, telemetry, and command data across scopes. The API is served through Traefik, which the shipped compose binds to `127.0.0.1:2900`, so a default single-host install is reachable only from the host; a multi-user deployment exposes the web port, and the `AV:N` rating reflects that standard remote-operator exposure.
All paths require valid authentication, and the triggering permissions (`system`/`system_set`/`script_view`) are below the `admin`/`script_run`/plugin-install tiers where COSMOS gates code execution. In the open-source edition `authorize` checks only token validity and does not enforce the permission string, so any authenticated user can perform these requests.
Suggested fix
The fix is to treat the user-writable overlay as data, never code, and to gate the writers, applied uniformly:
• Load table and cmd/tlm definitions for code-execution paths from the read-only `targets/` tree only, or parse the `targets_modified` overlay …
Related
Alias · CVE (1)
- CVE-2026-77602 OpenC3 COSMOS provides the functionality needed to send commands to and receive data from one or more embedded systems. vulnerability
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness
References
- https://github.com/OpenC3/cosmos/security/advisories/GHSA-jjq7-m736-w977 web
- https://github.com/OpenC3/cosmos/pull/3488 web
- https://github.com/OpenC3/cosmos/commit/71943352a28128ef3e7e894319d97a656b5cd4f2 web
- https://github.com/OpenC3/cosmos/commit/7a1538a4626f82c0d1540fcaa27ffdcbbd71ff81 web
- https://github.com/OpenC3/cosmos package