Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions cmd/soroban-cli/src/commands/cache/actionlog/read.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fs, io, path::PathBuf};
use std::io;

use crate::config::{data, locator};

Expand All @@ -12,6 +12,10 @@ pub enum Error {
NotFound(String),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error("invalid cache entry ID \"{0}\": expected a ULID")]
InvalidId(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}

#[derive(Debug, clap::Parser, Clone)]
Expand All @@ -24,15 +28,69 @@ pub struct Cmd {

impl Cmd {
pub fn run(&self) -> Result<(), Error> {
let file = self.file()?;
let id: ulid::Ulid = self
.id
.parse()
.map_err(|_| Error::InvalidId(self.id.clone()))?;
let file = data::actions_dir()?
.join(id.to_string())
.with_extension("json");
tracing::debug!("reading file {}", file.display());
let mut file = fs::File::open(file).map_err(|_| Error::NotFound(self.id.clone()))?;
let mut stdout = io::stdout();
let _ = io::copy(&mut file, &mut stdout);
let mut f = std::fs::File::open(&file).map_err(|e| {
if e.kind() == io::ErrorKind::NotFound {
Error::NotFound(self.id.clone())
} else {
Error::Io(e)
}
})?;
io::copy(&mut f, &mut io::stdout())?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;

#[test]
#[serial]
fn path_traversal_via_dotdot_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("STELLAR_DATA_HOME", tmp.path());

Comment thread
fnando marked this conversation as resolved.
let outside = tmp.path().join("outside.json");
std::fs::write(&outside, r#"{"leaked":true}"#).unwrap();

let cmd = Cmd {
id: "../outside".to_string(),
};

assert!(
cmd.run().is_err(),
"expected an error for a path-traversal ID, but run() succeeded"
);
Comment thread
fnando marked this conversation as resolved.
}

#[test]
#[serial]
fn absolute_path_id_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("STELLAR_DATA_HOME", tmp.path());

Comment thread
fnando marked this conversation as resolved.
let outside = tmp.path().join("outside.json");
std::fs::write(&outside, r#"{"leaked":true}"#).unwrap();

let abs_id = outside
.to_str()
.unwrap()
.trim_end_matches(".json")
.to_string();
let cmd = Cmd { id: abs_id };

pub fn file(&self) -> Result<PathBuf, Error> {
Ok(data::actions_dir()?.join(&self.id).with_extension("json"))
assert!(
cmd.run().is_err(),
"expected an error for an absolute-path ID, but run() succeeded"
);
Comment thread
fnando marked this conversation as resolved.
}
}
Loading