@@ -60,14 +60,22 @@ pub fn bucket_dir() -> Result<std::path::PathBuf, Error> {
6060pub 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+
7179pub 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) ]
203211mod 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