Files
authforge/daemon/src/storage/safe_user.rs
michael eb208579e3 refactor(daemon): centralize username path-segment sanitizer
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>
2026-04-27 09:22:23 -07:00

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"
);
}
}
}