feat(daemon): storage::pending JSON read/write/clear with path-traversal guards
This commit is contained in:
102
daemon/src/storage/pending.rs
Normal file
102
daemon/src/storage/pending.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use authforge_common::types::PendingFlag;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum PendingError {
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid username: {0:?}")]
|
||||
InvalidUser(String),
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // wired through AppState in Task 2.15.
|
||||
pub(crate) struct PendingStore {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // wired through AppState in Task 2.15.
|
||||
impl PendingStore {
|
||||
pub fn new(dir: PathBuf) -> Self {
|
||||
Self { dir }
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
pub fn set(&self, user: &str, flag: &PendingFlag) -> Result<(), PendingError> {
|
||||
std::fs::create_dir_all(&self.dir)?;
|
||||
let path = self.user_path(user)?;
|
||||
let body = serde_json::to_vec_pretty(flag)?;
|
||||
std::fs::write(path, body)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear(&self, user: &str) -> Result<bool, PendingError> {
|
||||
let path = self.user_path(user)?;
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(e) => Err(PendingError::Io(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, user: &str) -> Result<Option<PendingFlag>, PendingError> {
|
||||
let path = self.user_path(user)?;
|
||||
match std::fs::read(&path) {
|
||||
Ok(b) => Ok(Some(serde_json::from_slice(&b)?)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(PendingError::Io(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use authforge_common::types::Method;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn flag() -> PendingFlag {
|
||||
PendingFlag {
|
||||
required_methods: vec![Method::Fido2],
|
||||
created_unix: 1,
|
||||
deadline_unix: 0,
|
||||
re_enroll: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_get_clear_roundtrip() {
|
||||
let d = tempdir().unwrap();
|
||||
let s = PendingStore::new(d.path().to_path_buf());
|
||||
assert!(s.get("alice").unwrap().is_none());
|
||||
s.set("alice", &flag()).unwrap();
|
||||
assert_eq!(s.get("alice").unwrap().unwrap(), flag());
|
||||
assert!(s.clear("alice").unwrap());
|
||||
assert!(s.get("alice").unwrap().is_none());
|
||||
assert!(!s.clear("alice").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_path_traversal() {
|
||||
let d = tempdir().unwrap();
|
||||
let s = PendingStore::new(d.path().to_path_buf());
|
||||
for evil in ["../etc", "a/b", "", ".", "..", "x\0y"] {
|
||||
assert!(s.set(evil, &flag()).is_err());
|
||||
assert!(s.get(evil).is_err());
|
||||
assert!(s.clear(evil).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user