Security (EVM-18): Config derives Debug with terra_mnemonic field; any debug!(?cfg) leaks mnemonic #13

Closed
opened 2026-04-21 08:15:56 +00:00 by Brouie · 5 comments
Brouie commented 2026-04-21 08:15:56 +00:00 (Migrated from gitlab.com)

@PlasticDigits finding from security review on #5.

Severity

Medium. Secrets exposure via a single debug statement.

Description

oracle-service/src/config.rs:

#[derive(Debug, Clone)]
pub struct Config {
    pub bsc_rpc_urls: Vec<String>,
    pub venus_vtoken_address: String,
    pub terra_lcd_url: String,
    pub terra_chain_id: String,
    pub terra_mnemonic: String,        // <-- secret
    pub oracle_contract: String,
    pub poll_interval_secs: u64,
}

#[derive(Debug)] on a struct containing a mnemonic means any of the following leaks the full seed phrase:

  • debug!("{:?}", cfg)
  • info!(?cfg, "starting")
  • println!("{:#?}", cfg)
  • .context(format!("{:?}", cfg)) in error chains
  • Panic handlers that dump caller locals via debug formatter

No such call exists in main.rs today, but this is a one-line-change away from incident. Operator secrets (CL8Y bridge's docs/operator-secrets.md spec) must not be formattable.

Why this matters

The mnemonic controls the oracle operator key. With the key, an attacker can:

  • Submit UpdateRate calls (still bounded by on-chain caps, but see EVM-01/EVM-02 notes — the caps don't stop gradual manipulation if the attacker is patient)
  • DoS the oracle by running the service themselves in parallel, racing legitimate updates

Logs are often shipped to remote aggregators (datadog, cloudwatch, grafana), increasing exposure.

Recommendation

Remove Debug derive, or implement Debug manually with mnemonic redacted:

#[derive(Clone)]  // drop Debug
pub struct Config { ... }

impl std::fmt::Debug for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Config")
            .field("bsc_rpc_urls", &self.bsc_rpc_urls)
            .field("venus_vtoken_address", &self.venus_vtoken_address)
            .field("terra_lcd_url", &self.terra_lcd_url)
            .field("terra_chain_id", &self.terra_chain_id)
            .field("terra_mnemonic", &"<redacted>")
            .field("oracle_contract", &self.oracle_contract)
            .field("poll_interval_secs", &self.poll_interval_secs)
            .finish()
    }
}

Alternatively wrap the mnemonic in a secrecy::SecretString from the secrecy crate, which redacts on Debug automatically and requires explicit .expose_secret() to access.

  • Review parent: #5
@PlasticDigits finding from security review on #5. ## Severity Medium. Secrets exposure via a single debug statement. ## Description `oracle-service/src/config.rs`: ```rust #[derive(Debug, Clone)] pub struct Config { pub bsc_rpc_urls: Vec<String>, pub venus_vtoken_address: String, pub terra_lcd_url: String, pub terra_chain_id: String, pub terra_mnemonic: String, // <-- secret pub oracle_contract: String, pub poll_interval_secs: u64, } ``` `#[derive(Debug)]` on a struct containing a mnemonic means any of the following leaks the full seed phrase: - `debug!("{:?}", cfg)` - `info!(?cfg, "starting")` - `println!("{:#?}", cfg)` - `.context(format!("{:?}", cfg))` in error chains - Panic handlers that dump caller locals via debug formatter No such call exists in `main.rs` today, but this is a one-line-change away from incident. Operator secrets (CL8Y bridge's `docs/operator-secrets.md` spec) must not be formattable. ## Why this matters The mnemonic controls the oracle operator key. With the key, an attacker can: - Submit `UpdateRate` calls (still bounded by on-chain caps, but see EVM-01/EVM-02 notes — the caps don't stop gradual manipulation if the attacker is patient) - DoS the oracle by running the service themselves in parallel, racing legitimate updates Logs are often shipped to remote aggregators (datadog, cloudwatch, grafana), increasing exposure. ## Recommendation Remove `Debug` derive, or implement `Debug` manually with mnemonic redacted: ```rust #[derive(Clone)] // drop Debug pub struct Config { ... } impl std::fmt::Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Config") .field("bsc_rpc_urls", &self.bsc_rpc_urls) .field("venus_vtoken_address", &self.venus_vtoken_address) .field("terra_lcd_url", &self.terra_lcd_url) .field("terra_chain_id", &self.terra_chain_id) .field("terra_mnemonic", &"<redacted>") .field("oracle_contract", &self.oracle_contract) .field("poll_interval_secs", &self.poll_interval_secs) .finish() } } ``` Alternatively wrap the mnemonic in a `secrecy::SecretString` from the `secrecy` crate, which redacts on Debug automatically and requires explicit `.expose_secret()` to access. ## Related - Review parent: #5
Brouie commented 2026-04-21 08:17:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #5

mentioned in issue #5
PlasticDigits commented 2026-04-21 13:47:59 +00:00 (Migrated from gitlab.com)

mentioned in commit 673ac74579

mentioned in commit 673ac74579ff54283736cced27630183de913fae
PlasticDigits commented 2026-04-21 13:48:12 +00:00 (Migrated from gitlab.com)

Done (pushed to main)

Debug / logging and mnemonics

  • Replaced #[derive(Debug)] on Config and TerraSignerConfig with manual std::fmt::Debug implementations that print terra_mnemonic / mnemonic as "<redacted>" so tracing (debug!, info!(?…), etc.), format!("{:?}", …), and panic-style dumps cannot leak the seed phrase.
  • Added config::tests::config_debug_redacts_mnemonic to assert the formatted Debug string never contains known mnemonic words and includes the redaction marker.

Build fix (related)

  • main had bsc::read_exchange_rate_stored taking chain-id parameters while main.rs still called the old 3-argument form. Wired verify_all_bsc_rpc_urls at startup and passed allowed_bsc_chain_ids into read_exchange_rate_stored so the oracle service compiles and enforces BSC_ALLOWED_CHAIN_IDS consistently.

Commit: 673ac74.

## Done (pushed to `main`) **Debug / logging and mnemonics** - Replaced `#[derive(Debug)]` on `Config` and `TerraSignerConfig` with manual `std::fmt::Debug` implementations that print `terra_mnemonic` / `mnemonic` as `"<redacted>"` so `tracing` (`debug!`, `info!(?…)`, etc.), `format!("{:?}", …)`, and panic-style dumps cannot leak the seed phrase. - Added `config::tests::config_debug_redacts_mnemonic` to assert the formatted `Debug` string never contains known mnemonic words and includes the redaction marker. **Build fix (related)** - `main` had `bsc::read_exchange_rate_stored` taking chain-id parameters while `main.rs` still called the old 3-argument form. Wired `verify_all_bsc_rpc_urls` at startup and passed `allowed_bsc_chain_ids` into `read_exchange_rate_stored` so the oracle service compiles and enforces `BSC_ALLOWED_CHAIN_IDS` consistently. Commit: `673ac74`.
PlasticDigits commented 2026-04-21 13:52:19 +00:00 (Migrated from gitlab.com)

mentioned in commit 7cd0c45181

mentioned in commit 7cd0c4518109fe5ba25460808f2aefe87d2085ac
Brouie commented 2026-04-22 00:32:51 +00:00 (Migrated from gitlab.com)

@PlasticDigits verified fix on 673ac74 + 7cd0c45.

  • Config::terra_mnemonic + TerraSignerConfig::mnemonic now typed as secrecy::SecretString
  • Debug derive kept on Config because SecretString auto-redacts (prints "REDACTED"/"SecretBox"/"SecretString" instead of the value)
  • Access requires explicit expose_secret() — compile-time safety against accidental read
  • Boxed-str storage (into_boxed_str) — minor hardening via heap allocation rather than stack
  • Unit test asserts Debug output contains redacted-style marker, not mnemonic words

This is the stronger of the two recommended options from the finding (wrap-type > manual Debug impl). Closing.

@PlasticDigits verified fix on 673ac74 + 7cd0c45. - `Config::terra_mnemonic` + `TerraSignerConfig::mnemonic` now typed as `secrecy::SecretString` - `Debug` derive kept on `Config` because `SecretString` auto-redacts (prints "REDACTED"/"SecretBox"/"SecretString" instead of the value) - Access requires explicit `expose_secret()` — compile-time safety against accidental read - Boxed-str storage (`into_boxed_str`) — minor hardening via heap allocation rather than stack - Unit test asserts Debug output contains redacted-style marker, not mnemonic words This is the stronger of the two recommended options from the finding (wrap-type > manual Debug impl). Closing.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-22 00:32:53 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/ust1-window#13
No description provided.