fix: audit fixes — symlink-safe avatars, blur downscale + padding, config validation (v0.8.0)
Update PKGBUILD version / update-pkgver (push) Successful in 2s

- Replace canonicalize() with symlink_metadata + is_file + !is_symlink for avatar
  lookup (prevents symlink traversal to arbitrary files)
- Fix blur padding offset from (0,0) to (-pad,-pad) to prevent edge darkening
- Add MAX_BLUR_DIMENSION (1920px) downscale before GPU blur
- Validate blur per config source (invalid user value preserves system default)
- Wallpaper: use symlink_metadata + is_file + !is_symlink in resolve_background_path
This commit is contained in:
2026-03-30 16:08:50 +02:00
parent f01c6bd25d
commit 8aca2bf331
5 changed files with 66 additions and 40 deletions
+29 -19
View File
@@ -47,32 +47,34 @@ pub fn get_avatar_path(home: &Path, username: Option<&str>) -> Option<PathBuf> {
}
/// Find avatar with configurable AccountsService dir (for testing).
/// Rejects symlinks to prevent path traversal.
pub fn get_avatar_path_with(
home: &Path,
username: Option<&str>,
accountsservice_dir: &Path,
) -> Option<PathBuf> {
// ~/.face takes priority — canonicalize to resolve symlinks
// ~/.face takes priority
let face = home.join(".face");
if face.exists() {
if let Ok(canonical) = std::fs::canonicalize(&face) {
log::debug!("Avatar: using ~/.face ({})", canonical.display());
return Some(canonical);
if let Ok(meta) = face.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar: {}", face.display());
} else if meta.is_file() {
log::debug!("Avatar: using ~/.face ({})", face.display());
return Some(face);
}
// canonicalize failed (e.g. permissions) — skip rather than return unresolved symlink
log::warn!("Avatar: ~/.face exists but canonicalize failed, skipping");
}
// AccountsService icon — also canonicalize for consistency
// AccountsService icon fallback
if let Some(name) = username {
if accountsservice_dir.exists() {
let icon = accountsservice_dir.join(name);
if icon.exists() {
if let Ok(canonical) = std::fs::canonicalize(&icon) {
log::debug!("Avatar: using AccountsService icon ({})", canonical.display());
return Some(canonical);
if let Ok(meta) = icon.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar: {}", icon.display());
} else if meta.is_file() {
log::debug!("Avatar: using AccountsService icon ({})", icon.display());
return Some(icon);
}
log::warn!("Avatar: AccountsService icon exists but canonicalize failed, skipping");
}
}
}
@@ -107,8 +109,7 @@ mod tests {
let face = dir.path().join(".face");
fs::write(&face, "fake image").unwrap();
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
let expected = fs::canonicalize(&face).unwrap();
assert_eq!(path, Some(expected));
assert_eq!(path, Some(face));
}
#[test]
@@ -119,8 +120,7 @@ mod tests {
let icon = icons_dir.join("testuser");
fs::write(&icon, "fake image").unwrap();
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
let expected = fs::canonicalize(&icon).unwrap();
assert_eq!(path, Some(expected));
assert_eq!(path, Some(icon));
}
#[test]
@@ -133,8 +133,18 @@ mod tests {
let icon = icons_dir.join("testuser");
fs::write(&icon, "fake image").unwrap();
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
let expected = fs::canonicalize(&face).unwrap();
assert_eq!(path, Some(expected));
assert_eq!(path, Some(face));
}
#[test]
fn rejects_symlink_avatar() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("secret");
fs::write(&target, "secret content").unwrap();
let face = dir.path().join(".face");
std::os::unix::fs::symlink(&target, &face).unwrap();
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
assert!(path.is_none());
}
#[test]