Skip to content

Commit b81cf9e

Browse files
committed
Stop echoing a mistyped secret in address errors.
1 parent 7fcc73f commit b81cf9e

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

‎cmd/crates/soroban-test/tests/it/config.rs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,36 @@ fn keys_fund_error_does_not_leak_secret_bearing_input() {
12821282
.stderr(predicate::str::contains(MISTYPED_SECRET).not());
12831283
}
12841284

1285+
#[test]
1286+
fn token_address_param_error_does_not_leak_secret_bearing_input() {
1287+
// `--account`/`--to`/`--spender` share `UnresolvedScAddress`; a mistyped
1288+
// secret pasted there resolves before any network call, so the "not found"
1289+
// error must not echo it — in text or JSON output. `token balance --account`
1290+
// stands in for the shared resolve path.
1291+
let sandbox = TestEnv::default();
1292+
for output in ["text", "json"] {
1293+
sandbox
1294+
.new_assert_cmd("token")
1295+
.args([
1296+
"balance",
1297+
"--id",
1298+
"native",
1299+
"--account",
1300+
MISTYPED_SECRET,
1301+
"--output",
1302+
output,
1303+
"--rpc-url",
1304+
"http://localhost:1",
1305+
"--network-passphrase",
1306+
"Test SDF Network ; September 2015",
1307+
])
1308+
.assert()
1309+
.failure()
1310+
.stdout(predicate::str::contains(MISTYPED_SECRET).not())
1311+
.stderr(predicate::str::contains(MISTYPED_SECRET).not());
1312+
}
1313+
}
1314+
12851315
#[test]
12861316
fn help_conceals_sensitive_env_var_values() {
12871317
// Each concealed env var, paired with a subcommand whose `--help` renders it.

‎cmd/soroban-cli/src/config/sc_address.rs‎

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,25 @@ use super::{alias, key, locator, UnresolvedContract};
66

77
/// `ScAddress` can be either a resolved `xdr::ScAddress` or an alias of a `Contract` or `MuxedAccount`.
88
#[allow(clippy::module_name_repetitions)]
9-
#[derive(Clone, Debug)]
9+
#[derive(Clone)]
1010
pub enum UnresolvedScAddress {
1111
Resolved(xdr::ScAddress),
1212
Alias(String),
1313
}
1414

15+
impl std::fmt::Debug for UnresolvedScAddress {
16+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17+
match self {
18+
UnresolvedScAddress::Resolved(addr) => f.debug_tuple("Resolved").field(addr).finish(),
19+
// Never echo the raw input: it may be a secret key or seed phrase
20+
// pasted where an alias was expected.
21+
UnresolvedScAddress::Alias(_) => {
22+
f.debug_tuple("Alias").field(&"<alias or secret>").finish()
23+
}
24+
}
25+
}
26+
}
27+
1528
impl Default for UnresolvedScAddress {
1629
fn default() -> Self {
1730
UnresolvedScAddress::Alias(String::default())
@@ -24,8 +37,10 @@ pub enum Error {
2437
Locator(#[from] locator::Error),
2538
#[error(transparent)]
2639
Key(#[from] key::Error),
27-
#[error("Account alias \"{0}\" not Found")]
28-
AccountAliasNotFound(String),
40+
// The input is never echoed: a secret key or seed phrase mistyped where an
41+
// address was expected must not reach the terminal, logs, or JSON output.
42+
#[error("invalid address or alias")]
43+
AccountAliasNotFound,
2944
#[error("alias '{0}' is reserved for the native asset contract but also matches a stored key; pass an explicit contract (C...) or account (G...) address instead")]
3045
ReservedAliasShadowsKey(String),
3146
}
@@ -116,7 +131,7 @@ impl UnresolvedScAddress {
116131
xdr::ScAddress::MuxedAccount(xdr::MuxedEd25519Account { id, ed25519 })
117132
}
118133
}),
119-
_ => Err(Error::AccountAliasNotFound(alias)),
134+
_ => Err(Error::AccountAliasNotFound),
120135
}
121136
}
122137
}
@@ -307,4 +322,42 @@ mod tests {
307322
!UnresolvedScAddress::Alias("dual".to_string()).is_muxed(&locator, network_passphrase)
308323
);
309324
}
325+
326+
// A valid strkey charset with a bad checksum: fails to parse as an address
327+
// yet is shaped like a secret key, so it falls through to the not-found arm.
328+
const MISTYPED_SECRET: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BX";
329+
// A seed phrase with an invalid final word: fails to derive a key, so it too
330+
// falls through rather than resolving to an account.
331+
const MALFORMED_SEED: &str =
332+
"illness spike retreat truth genius clock brain pass fit cave bargain xyzzy";
333+
334+
#[test]
335+
fn resolve_does_not_leak_secret_bearing_input() {
336+
let dir = tempfile::tempdir().unwrap();
337+
let locator = locator::Args {
338+
config_dir: Some(dir.path().to_path_buf()),
339+
};
340+
let network_passphrase = "Test Network";
341+
342+
// A mistyped secret key or seed phrase pasted where an address was
343+
// expected must never be echoed back in the not-found error.
344+
for input in [MISTYPED_SECRET, MALFORMED_SEED] {
345+
let err = UnresolvedScAddress::from_str(input)
346+
.unwrap()
347+
.resolve(&locator, network_passphrase, None)
348+
.unwrap_err();
349+
assert!(
350+
!err.to_string().contains(input),
351+
"error leaked secret-bearing input: {err}"
352+
);
353+
}
354+
}
355+
356+
#[test]
357+
fn debug_conceals_secret_bearing_input() {
358+
for input in [MISTYPED_SECRET, MALFORMED_SEED] {
359+
let address = UnresolvedScAddress::from_str(input).unwrap();
360+
assert!(!format!("{address:?}").contains(input));
361+
}
362+
}
310363
}

0 commit comments

Comments
 (0)