release: v0.7.1 — Security-Patch (R-01 bis R-06)
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
- R-01: Bereinigung verbliebener Restbehauptungen in Doku und Code (LEGAL.md, README.md, QUICKSTART.md, main.rs, crypto/storage/recovery/verify.rs) - R-02: Lückenloser Carrier-Schutz in SanctumFs::copy (Quelle & Ziel) und sync (Pull-Skip & Push-Schutz) - R-03: Shared Dateinamen-Validierung (validate_node_name) in pathutil.rs, durchgesetzt in storage.rs und vfs.rs - R-04: Fail-closed Release-Packaging & obligatorische Minisign-Signatur in CI (.gitea/workflows/release.yaml) und Scripts - R-05: Session-Token Beseitigung im argv: In-Process Win32 WNetAddConnection2W/WNetCancelConnection2W, kein gio argv-Token, Multi-Auth HTTP Middleware (Basic Auth, X-Sanctum-Token, Path-Fallback) mit 401 WWW-Authenticate - R-06: Sofortiges Löschen von SANCTUM_RECOVERY_KEY aus der Prozessumgebung
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Validiert einen Datei- oder Verzeichnisnamen gegen Path-Traversal, Null-Bytes,
|
||||
/// unzulässige Steuerzeichen und Windows-reservierte Gerätenamen (S-08, R-03).
|
||||
pub fn validate_node_name(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("Dateiname darf nicht leer sein.");
|
||||
}
|
||||
if name == "." || name == ".." {
|
||||
bail!("Ungültiger Dateiname (Verzeichnisreferenz verboten): '{}'", name);
|
||||
}
|
||||
if name.contains('/') || name.contains('\\') || name.contains('\0') {
|
||||
bail!(
|
||||
"Dateiname enthält unzulässige Trennzeichen oder Null-Bytes: '{}'",
|
||||
name
|
||||
);
|
||||
}
|
||||
for c in name.chars() {
|
||||
if (c as u32) < 0x20 {
|
||||
bail!("Dateiname enthält Steuerzeichen: '{}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
// Windows reservierte Gerätenamen (CON, PRN, AUX, NUL, COM1..9, LPT1..9)
|
||||
let base_name = if let Some(dot_idx) = name.find('.') {
|
||||
&name[..dot_idx]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let base_upper = base_name.to_ascii_uppercase();
|
||||
let reserved = [
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
];
|
||||
if reserved.contains(&base_upper.as_str()) {
|
||||
bail!(
|
||||
"Dateiname '{}' kollidiert mit einem reservierten Windows-Gerätenamen.",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_node_name() {
|
||||
assert!(validate_node_name("test.txt").is_ok());
|
||||
assert!(validate_node_name("").is_err());
|
||||
assert!(validate_node_name(".").is_err());
|
||||
assert!(validate_node_name("..").is_err());
|
||||
assert!(validate_node_name("foo/bar").is_err());
|
||||
assert!(validate_node_name("foo\\bar").is_err());
|
||||
assert!(validate_node_name("CON").is_err());
|
||||
assert!(validate_node_name("aux.txt").is_err());
|
||||
assert!(validate_node_name("nul").is_err());
|
||||
assert!(validate_node_name("com1.log").is_err());
|
||||
assert!(validate_node_name("line\nbreak").is_err());
|
||||
assert!(validate_node_name("null\0byte").is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user