Affected: station-v0.78 · Class: improper signature verification / authentication bypass (CWE-347) · Severity: HIGH · Signed receipt: attached (railcall verify → SIGNATURE VALID, offline)
The two facts that compose to zero publisher authentication (default install)
- The signature is verified against the key embedded in the module's own manifest.
_verify_module_signature(modules_routes.py:145-176) readspubkey_hex = manifest.get("publisher_pubkey")(line 160) and verifiesmodule.sigagainst THAT key. There is no comparison anywhere in the load path against a pinned or marketplace-registered publisher key. An attacker generates their own Ed25519 keypair, embeds their pubkey inpublisher_pubkey, and signs — the verify passes. It proves "whoever built this held a private key", never "a known publisher signed this." Self-referential. - The trust gate is off by default. After verify,
_load_modulescallspublisher_trust.is_trusted(WS, manifest.publisher_pubkey)(modules_routes.py:854). The free-install default istrust_mode=anywith an empty allowlist (publisher_trust.py:15, 26; default returned at :68/:73/:80). In that modeis_trustedreturns{"trusted": True, "reason": "any valid signature accepted"}for any key (publisher_trust.py:151-156).
Proof (container, real station function)
is_trusted("ab"*32) # a 64-hex key no registered publisher owns, invented on the spot
=> {'trusted': True, 'mode': 'any', 'publisher_name': None,
'reason': 'trust_mode=any (any valid signature accepted)'}
So an arbitrary attacker key is "trusted" by default, and the signature that would "authenticate" it is one the attacker made over their own key. A module bearing a self-consistent, attacker-generated signature passes BOTH gates and loads — and loaded modules run at full privilege (module_sandbox.py:3-6; the opt-in capability block is off by default — this is the already-reported F7 that supplies the impact).
Distinct from the already-reported "modules run full-privilege by default" (F7)
F7 is about privilege (CWE-250: what a module can do once loaded) and named the publisher-trust allowlist as "the primary defense." This is a distinct authentication failure (CWE-347): the signature+trust chain that is supposed to establish which publisher a module is from is, at the default config, satisfied by any self-signed bundle — because the signature checks the manifest's own key and the default trust mode accepts every key. The material change: the mechanism advertised to authenticate module publishers does not.
Honest scope (not drive-by RCE)
- Exploitation requires the operator to install or place a module and trigger a load/reload — the realistic vector is a sideloaded module (a dir/tarball shared via a repo, a colleague, a contest entry, a "try my module" link), which is verified+trusted locally with no marketplace review.
- The official marketplace adds account + publisher-key registration and human review before listing, so this is not remote drive-by RCE through that channel; the finding is that the cryptographic signature+trust gate behind a local install is a no-op, so security on the local path rests entirely on the operator not sideloading.
Fix
- Verify
module.sigagainst a pinned / marketplace-registered publisher key (the key registered viarailcall market publisher register), not the manifest-embedded key; reject when the embedded key isn't the registered one. - Default
trust_modetoallowlist(first-party publishers pre-trusted) so an unknown publisher key requires an explicit operatorrailcall trust add— fail-closed, not fail-open.
Found and reported by @marcofgv. Reviewed adversarially (codex novelty+overclaim triage) before posting.
Signed receipt (railcall verify → SIGNATURE VALID, offline)
{
"schema": "railcall_audit_receipt.v1",
"ran_at": "2026-08-11T21:06:01",
"file": {
"name": "findings_trust.csv",
"sha256": "sha256:8ed99d517420ed3b5e1563199a1719ac1a2eee505f809e8c46c78c20c3ada02b",
"bytes": 235
},
"audit": {
"rows": 1,
"columns": 4,
"import_breakers": 0,
"pii_columns": 0,
"formula_injection_cells": 0,
"findings": []
},
"network_audit": {
"lsof_available": false,
"error": "lsof_not_found",
"external_sockets_open": null
},
"result": "audited",
"receipt_version": "v2",
"flow": {
"dry_run": true,
"name": "audit",
"action_type": "audit"
},
"governance": {
"policy_ref": "none",
"policy_hash": "ff56072e81ed4908ea91f567741238b387e536cd1f5974513ee18df0d5c575b9",
"approval_chain": [],
"risk_classification": "unknown",
"irreversible": false
},
"execution": {
"input_sha256": "sha256:8ed99d517420ed3b5e1563199a1719ac1a2eee505f809e8c46c78c20c3ada02b",
"output_sha256": "",
"duration_ms": 0,
"exit_code": 0
},
"signer_alg": "ed25519",
"public_key_hex": "ea2446fec9cc4de478c853fb35c778262d4327ac7d32d6ccff36bdbbfcd775e2",
"signature_hex": "07dbbfaaabffef87847d70aca2ebde5271e6a3fd6c730eee037d75f2688d6d4572148c73b1a4546a74c2d7e4b5c8080cd0c55885353a0cfc8d658a58402c0901"
}```