Extract the inline path-traversal check from PendingStore into a shared storage::safe_user::join_user_segment helper. RecoveryStore (next) reuses it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
46 lines
1.2 KiB
Rust
46 lines
1.2 KiB
Rust
//! Shared username sanitizer for path-segment use across pending/recovery stores.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub(crate) enum SegmentError {
|
|
#[error("invalid username segment: {0:?}")]
|
|
Invalid(String),
|
|
}
|
|
|
|
/// Reject any username that could escape a single path segment. Centralized
|
|
/// so pending / recovery / future stores don't drift on what's "safe."
|
|
pub(crate) fn join_user_segment(dir: &Path, user: &str) -> Result<PathBuf, SegmentError> {
|
|
if user.is_empty()
|
|
|| user == "."
|
|
|| user == ".."
|
|
|| user.contains('/')
|
|
|| user.contains('\0')
|
|
{
|
|
return Err(SegmentError::Invalid(user.to_string()));
|
|
}
|
|
Ok(dir.join(user))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn accepts_normal_username() {
|
|
let p = join_user_segment(Path::new("/tmp/x"), "alice").unwrap();
|
|
assert_eq!(p, PathBuf::from("/tmp/x/alice"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_traversal_and_separators() {
|
|
for evil in ["", ".", "..", "a/b", "../etc", "x\0y"] {
|
|
assert!(
|
|
join_user_segment(Path::new("/tmp/x"), evil).is_err(),
|
|
"{evil:?} should be rejected"
|
|
);
|
|
}
|
|
}
|
|
}
|