Skip to content

Commit c6557d3

Browse files
authored
Redact password in cached RPC URLs. (#2531)
1 parent 41a2885 commit c6557d3

1 file changed

Lines changed: 104 additions & 14 deletions

File tree

cmd/soroban-cli/src/config/data.rs

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,22 @@ pub fn bucket_dir() -> Result<std::path::PathBuf, Error> {
6060
pub fn write(action: Action, rpc_url: &Url) -> Result<ulid::Ulid, Error> {
6161
let data = Data {
6262
action,
63-
rpc_url: rpc_url.to_string(),
63+
rpc_url: redact_userinfo(rpc_url).to_string(),
6464
};
6565
let id = ulid::Ulid::new();
6666
let file = actions_dir()?.join(id.to_string()).with_extension("json");
6767
std::fs::write(file, serde_json::to_string(&data)?)?;
6868
Ok(id)
6969
}
7070

71+
fn redact_userinfo(url: &Url) -> Url {
72+
let mut redacted = url.clone();
73+
if redacted.password().is_some() {
74+
let _ = redacted.set_password(Some("redacted"));
75+
}
76+
redacted
77+
}
78+
7179
pub fn read(id: &ulid::Ulid) -> Result<(Action, Url), Error> {
7280
let file = actions_dir()?.join(id.to_string()).with_extension("json");
7381
let data: Data = serde_json::from_str(&std::fs::read_to_string(file)?)?;
@@ -202,25 +210,107 @@ fn to_xdr(data: &impl WriteXdr) -> Result<String, xdr::Error> {
202210
#[cfg(test)]
203211
mod test {
204212
use super::*;
213+
use crate::test_utils::with_env_set;
205214
use serial_test::serial;
206215

207216
#[test]
208217
#[serial]
209218
fn test_write_read() {
210219
let t = assert_fs::TempDir::new().unwrap();
211-
std::env::set_var("STELLAR_DATA_HOME", t.path().to_str().unwrap());
212-
let rpc_uri = Url::from_str("http://localhost:8000").unwrap();
213-
let sim = SimulateTransactionResponse::default();
214-
let original_action: Action = sim.into();
215-
216-
let id = write(original_action.clone(), &rpc_uri.clone()).unwrap();
217-
let (action, new_rpc_uri) = read(&id).unwrap();
218-
assert_eq!(rpc_uri, new_rpc_uri);
219-
match (action, original_action) {
220-
(Action::Simulate { response: a }, Action::Simulate { response: b }) => {
221-
assert_eq!(a.min_resource_fee, b.min_resource_fee);
220+
with_env_set("STELLAR_DATA_HOME", t.path(), || {
221+
let rpc_uri = Url::from_str("http://localhost:8000").unwrap();
222+
let sim = SimulateTransactionResponse::default();
223+
let original_action: Action = sim.into();
224+
225+
let id = write(original_action.clone(), &rpc_uri.clone()).unwrap();
226+
let (action, new_rpc_uri) = read(&id).unwrap();
227+
assert_eq!(rpc_uri, new_rpc_uri);
228+
match (action, original_action) {
229+
(Action::Simulate { response: a }, Action::Simulate { response: b }) => {
230+
assert_eq!(a.min_resource_fee, b.min_resource_fee);
231+
}
232+
_ => panic!("Action mismatch"),
222233
}
223-
_ => panic!("Action mismatch"),
224-
}
234+
});
235+
}
236+
237+
#[test]
238+
#[serial]
239+
fn actionlog_write_redacts_rpc_url_password_on_disk() {
240+
let t = assert_fs::TempDir::new().unwrap();
241+
with_env_set("STELLAR_DATA_HOME", t.path(), || {
242+
let rpc_uri =
243+
Url::from_str("https://alice:supersecret@rpc.example.com/soroban/rpc").unwrap();
244+
let action: Action = SimulateTransactionResponse::default().into();
245+
246+
let id = write(action, &rpc_uri).unwrap();
247+
let file = actions_dir()
248+
.unwrap()
249+
.join(id.to_string())
250+
.with_extension("json");
251+
let contents = std::fs::read_to_string(&file).unwrap();
252+
253+
assert!(
254+
!contents.contains("supersecret"),
255+
"password leaked into action-log JSON: {contents}"
256+
);
257+
assert!(
258+
contents.contains("alice"),
259+
"username should be preserved: {contents}"
260+
);
261+
assert!(
262+
contents.contains("redacted"),
263+
"expected literal `redacted` placeholder: {contents}"
264+
);
265+
assert!(
266+
contents.contains("rpc.example.com"),
267+
"expected host to be preserved: {contents}"
268+
);
269+
});
270+
}
271+
272+
#[test]
273+
fn redact_userinfo_leaves_url_without_password_unchanged() {
274+
let plain = Url::from_str("https://rpc.example.com/soroban/rpc").unwrap();
275+
assert_eq!(redact_userinfo(&plain), plain);
276+
277+
let user_only = Url::from_str("https://alice@rpc.example.com/soroban/rpc").unwrap();
278+
assert_eq!(redact_userinfo(&user_only), user_only);
279+
280+
let with_password =
281+
Url::from_str("https://alice:supersecret@rpc.example.com/soroban/rpc").unwrap();
282+
let redacted = redact_userinfo(&with_password);
283+
assert_eq!(redacted.username(), "alice");
284+
assert_eq!(redacted.password(), Some("redacted"));
285+
assert_eq!(redacted.host_str(), Some("rpc.example.com"));
286+
assert_eq!(redacted.path(), "/soroban/rpc");
287+
}
288+
289+
#[test]
290+
#[serial]
291+
fn actionlog_list_actions_renders_redacted_rpc_url() {
292+
let t = assert_fs::TempDir::new().unwrap();
293+
with_env_set("STELLAR_DATA_HOME", t.path(), || {
294+
let rpc_uri =
295+
Url::from_str("https://alice:supersecret@rpc.example.com/soroban/rpc").unwrap();
296+
let action: Action = SimulateTransactionResponse::default().into();
297+
298+
write(action, &rpc_uri).unwrap();
299+
let rendered = list_actions()
300+
.unwrap()
301+
.into_iter()
302+
.map(|entry| entry.to_string())
303+
.collect::<Vec<_>>()
304+
.join("\n");
305+
306+
assert!(
307+
!rendered.contains("supersecret"),
308+
"password leaked into ls -l render: {rendered}"
309+
);
310+
assert!(
311+
rendered.contains("alice:redacted"),
312+
"expected `alice:redacted` in ls -l render: {rendered}"
313+
);
314+
});
225315
}
226316
}

0 commit comments

Comments
 (0)