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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod credentials;
|
||||
pub(crate) mod pending;
|
||||
pub(crate) mod policy;
|
||||
pub(crate) mod safe_user;
|
||||
pub(crate) mod userdb;
|
||||
|
||||
@@ -24,15 +24,9 @@ impl PendingStore {
|
||||
}
|
||||
|
||||
fn user_path(&self, user: &str) -> Result<PathBuf, PendingError> {
|
||||
if user.is_empty()
|
||||
|| user.contains('/')
|
||||
|| user.contains('\0')
|
||||
|| user == "."
|
||||
|| user == ".."
|
||||
{
|
||||
return Err(PendingError::InvalidUser(user.to_string()));
|
||||
}
|
||||
Ok(self.dir.join(user))
|
||||
super::safe_user::join_user_segment(&self.dir, user).map_err(|e| match e {
|
||||
super::safe_user::SegmentError::Invalid(s) => PendingError::InvalidUser(s),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set(&self, user: &str, flag: &PendingFlag) -> Result<(), PendingError> {
|
||||
|
||||
45
daemon/src/storage/safe_user.rs
Normal file
45
daemon/src/storage/safe_user.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user