chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Release files are read only by a hash that is a SHA-256
crates/knotless/src/release.rs+8−0
| @@ -101,6 +101,14 @@ pub fn sha256(bytes: &[u8]) -> String { | ||
format!("{:x}", Sha256::digest(bytes)) | ||
} | ||
| ||
/// Whether `hash` is one [`sha256`] gives: 64 lowercase hex digits. A | ||
/// record's hash names the file its bytes are kept in, so one that is not | ||
/// could name a path outside where they are kept. | ||
pub fn valid_sha256(hash: &str) -> bool { | ||
let hex = |b: u8| matches!(b, b'0'..=b'9' | b'a'..=b'f'); | ||
hash.len() == 64 && hash.bytes().all(hex) | ||
} | ||
| ||
/// Publishes `spec` as the owner of `url`'s repository, logged in: its | ||
/// files uploaded, then its record created. Returns the record's AT-URI. | ||
/// A version published before is refused, before anything is uploaded, | ||
crates/knotless-serve/src/releases.rs+28−1
| @@ -146,7 +146,9 @@ impl Records { | ||
&self, | ||
file: &knotless::records::ReleaseFile, | ||
) -> anyhow::Result<PathBuf> { | ||
let at = self.kept.join(&file.sha256); | ||
let hash = &file.sha256; | ||
anyhow::ensure!(release::valid_sha256(hash), "{hash:?} is not a SHA-256"); | ||
let at = self.kept.join(hash); | ||
if tokio::fs::try_exists(&at).await? { | ||
return Ok(at); | ||
} | ||
| @@ -250,3 +252,28 @@ async fn file( | ||
Err(e) => failed(StatusCode::BAD_GATEWAY, e), | ||
} | ||
} | ||
| ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
| ||
// A record's hash names the file its bytes are kept in: one that is | ||
// not a SHA-256 is refused before it names a path. | ||
#[tokio::test] | ||
async fn a_file_is_kept_only_by_its_hash() { | ||
let ctx = Ctx::new(knotless::env::Env::from_vars(|_| None)); | ||
let url = RepoUrl { | ||
authority: "did:plc:a".into(), | ||
name: "r".into(), | ||
}; | ||
let records = Records::new(ctx, url, "p".into()); | ||
let file = knotless::records::ReleaseFile { | ||
name: "x".into(), | ||
size: 0, | ||
sha256: format!("{:.<64}", "../../../../etc/passwd"), | ||
parts: Vec::new(), | ||
}; | ||
let refused = records.fetch(&file).await.unwrap_err().to_string(); | ||
assert!(refused.contains("is not a SHA-256"), "{refused}"); | ||
} | ||
} | ||