fix: audit fixes — symlink-safe avatars, blur downscale + padding, config validation (v0.8.0)
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
All checks were successful
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:
parent
f01c6bd25d
commit
8aca2bf331
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -616,7 +616,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moonset"
|
name = "moonset"
|
||||||
version = "0.7.2"
|
version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonset"
|
name = "moonset"
|
||||||
version = "0.7.3"
|
version = "0.8.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Wayland session power menu with GTK4 and Layer Shell"
|
description = "Wayland session power menu with GTK4 and Layer Shell"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@ -37,8 +37,11 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
if parsed.background_path.is_some() {
|
if parsed.background_path.is_some() {
|
||||||
merged.background_path = parsed.background_path;
|
merged.background_path = parsed.background_path;
|
||||||
}
|
}
|
||||||
if parsed.background_blur.is_some() {
|
// Validate blur per source — invalid values preserve the previous default
|
||||||
|
if parsed.background_blur.is_some_and(|b| b.is_finite() && (0.0..=200.0).contains(&b)) {
|
||||||
merged.background_blur = parsed.background_blur;
|
merged.background_blur = parsed.background_blur;
|
||||||
|
} else if parsed.background_blur.is_some() {
|
||||||
|
log::warn!("Invalid background_blur in {}, ignoring", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -48,14 +51,6 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate blur range
|
|
||||||
if let Some(blur) = merged.background_blur {
|
|
||||||
if !blur.is_finite() || blur < 0.0 || blur > 200.0 {
|
|
||||||
log::warn!("Invalid background_blur value {blur}, ignoring");
|
|
||||||
merged.background_blur = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,14 +64,16 @@ pub fn resolve_background_path(config: &Config) -> Option<PathBuf> {
|
|||||||
|
|
||||||
/// Resolve with configurable moonarch wallpaper path (for testing).
|
/// Resolve with configurable moonarch wallpaper path (for testing).
|
||||||
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
|
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
|
||||||
// User-configured path
|
// User-configured path — reject symlinks to prevent path traversal
|
||||||
if let Some(ref bg) = config.background_path {
|
if let Some(ref bg) = config.background_path {
|
||||||
let path = PathBuf::from(bg);
|
let path = PathBuf::from(bg);
|
||||||
if path.is_file() {
|
if let Ok(meta) = path.symlink_metadata() {
|
||||||
|
if meta.is_file() && !meta.file_type().is_symlink() {
|
||||||
log::debug!("Wallpaper source: config ({})", path.display());
|
log::debug!("Wallpaper source: config ({})", path.display());
|
||||||
return Some(path);
|
return Some(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Moonarch ecosystem default
|
// Moonarch ecosystem default
|
||||||
if moonarch_wallpaper.is_file() {
|
if moonarch_wallpaper.is_file() {
|
||||||
|
|||||||
31
src/panel.rs
31
src/panel.rs
@ -103,11 +103,18 @@ pub fn load_background_texture(bg_path: Option<&Path>) -> Option<gdk::Texture> {
|
|||||||
|
|
||||||
// -- GPU blur via GskBlurNode -------------------------------------------------
|
// -- GPU blur via GskBlurNode -------------------------------------------------
|
||||||
|
|
||||||
|
/// Maximum texture dimension before downscaling for blur.
|
||||||
|
/// Keeps GPU work reasonable on 4K+ displays.
|
||||||
|
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
||||||
|
|
||||||
/// Render a blurred texture using the GPU via GskBlurNode.
|
/// Render a blurred texture using the GPU via GskBlurNode.
|
||||||
///
|
///
|
||||||
/// To avoid edge darkening (blur samples transparent pixels outside bounds),
|
/// To avoid edge darkening (blur samples transparent pixels outside bounds),
|
||||||
/// the texture is rendered with padding equal to 3x the blur sigma. The blur
|
/// the texture is rendered with padding equal to 3x the blur sigma. The blur
|
||||||
/// is applied to the padded area, then cropped back to the original size.
|
/// is applied to the padded area, then cropped back to the original size.
|
||||||
|
///
|
||||||
|
/// Large textures (> MAX_BLUR_DIMENSION) are downscaled before blurring to
|
||||||
|
/// reduce GPU work. The sigma is scaled proportionally.
|
||||||
fn render_blurred_texture(
|
fn render_blurred_texture(
|
||||||
widget: &impl IsA<gtk::Widget>,
|
widget: &impl IsA<gtk::Widget>,
|
||||||
texture: &gdk::Texture,
|
texture: &gdk::Texture,
|
||||||
@ -116,17 +123,29 @@ fn render_blurred_texture(
|
|||||||
let native = widget.native()?;
|
let native = widget.native()?;
|
||||||
let renderer = native.renderer()?;
|
let renderer = native.renderer()?;
|
||||||
|
|
||||||
let w = texture.width() as f32;
|
let orig_w = texture.width() as f32;
|
||||||
let h = texture.height() as f32;
|
let orig_h = texture.height() as f32;
|
||||||
|
|
||||||
|
// Downscale large textures to reduce GPU blur work
|
||||||
|
let max_dim = orig_w.max(orig_h);
|
||||||
|
let scale = if max_dim > MAX_BLUR_DIMENSION {
|
||||||
|
MAX_BLUR_DIMENSION / max_dim
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let w = (orig_w * scale).round();
|
||||||
|
let h = (orig_h * scale).round();
|
||||||
|
let scaled_sigma = sigma * scale;
|
||||||
|
|
||||||
// Padding must cover the blur kernel radius (typically ~3x sigma)
|
// Padding must cover the blur kernel radius (typically ~3x sigma)
|
||||||
let pad = (sigma * 3.0).ceil();
|
let pad = (scaled_sigma * 3.0).ceil();
|
||||||
|
|
||||||
let snapshot = gtk::Snapshot::new();
|
let snapshot = gtk::Snapshot::new();
|
||||||
// Clip output to original texture size
|
// Clip output to scaled texture size
|
||||||
snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h));
|
snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h));
|
||||||
snapshot.push_blur(sigma as f64);
|
snapshot.push_blur(scaled_sigma as f64);
|
||||||
// Render texture with padding on all sides (edges repeat via oversized bounds)
|
// Render texture with padding on all sides (edges repeat via oversized bounds)
|
||||||
snapshot.append_texture(texture, &graphene_rs::Rect::new(0.0, 0.0, w + 2.0 * pad, h + 2.0 * pad));
|
snapshot.append_texture(texture, &graphene_rs::Rect::new(-pad, -pad, w + 2.0 * pad, h + 2.0 * pad));
|
||||||
snapshot.pop(); // blur
|
snapshot.pop(); // blur
|
||||||
snapshot.pop(); // clip
|
snapshot.pop(); // clip
|
||||||
|
|
||||||
|
|||||||
48
src/users.rs
48
src/users.rs
@ -47,32 +47,34 @@ pub fn get_avatar_path(home: &Path, username: Option<&str>) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find avatar with configurable AccountsService dir (for testing).
|
/// Find avatar with configurable AccountsService dir (for testing).
|
||||||
|
/// Rejects symlinks to prevent path traversal.
|
||||||
pub fn get_avatar_path_with(
|
pub fn get_avatar_path_with(
|
||||||
home: &Path,
|
home: &Path,
|
||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
accountsservice_dir: &Path,
|
accountsservice_dir: &Path,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
// ~/.face takes priority — canonicalize to resolve symlinks
|
// ~/.face takes priority
|
||||||
let face = home.join(".face");
|
let face = home.join(".face");
|
||||||
if face.exists() {
|
if let Ok(meta) = face.symlink_metadata() {
|
||||||
if let Ok(canonical) = std::fs::canonicalize(&face) {
|
if meta.file_type().is_symlink() {
|
||||||
log::debug!("Avatar: using ~/.face ({})", canonical.display());
|
log::warn!("Rejecting symlink avatar: {}", face.display());
|
||||||
return Some(canonical);
|
} 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 let Some(name) = username {
|
||||||
if accountsservice_dir.exists() {
|
if accountsservice_dir.exists() {
|
||||||
let icon = accountsservice_dir.join(name);
|
let icon = accountsservice_dir.join(name);
|
||||||
if icon.exists() {
|
if let Ok(meta) = icon.symlink_metadata() {
|
||||||
if let Ok(canonical) = std::fs::canonicalize(&icon) {
|
if meta.file_type().is_symlink() {
|
||||||
log::debug!("Avatar: using AccountsService icon ({})", canonical.display());
|
log::warn!("Rejecting symlink avatar: {}", icon.display());
|
||||||
return Some(canonical);
|
} 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");
|
let face = dir.path().join(".face");
|
||||||
fs::write(&face, "fake image").unwrap();
|
fs::write(&face, "fake image").unwrap();
|
||||||
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
|
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
|
||||||
let expected = fs::canonicalize(&face).unwrap();
|
assert_eq!(path, Some(face));
|
||||||
assert_eq!(path, Some(expected));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -119,8 +120,7 @@ mod tests {
|
|||||||
let icon = icons_dir.join("testuser");
|
let icon = icons_dir.join("testuser");
|
||||||
fs::write(&icon, "fake image").unwrap();
|
fs::write(&icon, "fake image").unwrap();
|
||||||
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
||||||
let expected = fs::canonicalize(&icon).unwrap();
|
assert_eq!(path, Some(icon));
|
||||||
assert_eq!(path, Some(expected));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -133,8 +133,18 @@ mod tests {
|
|||||||
let icon = icons_dir.join("testuser");
|
let icon = icons_dir.join("testuser");
|
||||||
fs::write(&icon, "fake image").unwrap();
|
fs::write(&icon, "fake image").unwrap();
|
||||||
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
||||||
let expected = fs::canonicalize(&face).unwrap();
|
assert_eq!(path, Some(face));
|
||||||
assert_eq!(path, Some(expected));
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user