Fix 29 audit findings across all severity tiers

Critical: fix unsquashfs arg order, quote Exec paths with spaces,
fix compare_versions antisymmetry, chunk-based signature detection,
bounded ELF header reads.

High: handle NULL CVE severity, prevent pipe deadlock in inspector,
fix glob_match edge case, fix backup archive path collisions, async
crash detection with stderr capture.

Medium: gate scan on auto-scan setting, fix window size persistence,
fix announce() for Stack containers, claim lightbox gesture, use
serde_json for CLI output, remove dead CSS @media blocks, add
detail-tab persistence, remove invalid metainfo categories, byte-level
fuse signature search.

Low: tighten Wayland env var detection, ELF magic validation,
timeout for update info extraction, quoted arg parsing, stop watcher
timer on window destroy, GSettings choices/range constraints, remove
unused CSS classes, define status-ok/status-attention CSS.
This commit is contained in:
lashman
2026-02-27 22:08:53 +02:00
parent f87403794e
commit e9343da249
27 changed files with 1737 additions and 250 deletions

View File

@@ -370,27 +370,51 @@ fn parse_elf32_sections(data: &[u8]) -> Option<String> {
}
/// Fallback: run the AppImage with --appimage-updateinformation flag.
/// Uses a 5-second timeout to avoid hanging on apps with custom AppRun scripts.
fn extract_update_info_runtime(path: &Path) -> Option<String> {
let output = std::process::Command::new(path)
let mut child = std::process::Command::new(path)
.arg("--appimage-updateinformation")
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
.output()
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
if output.status.success() {
let info = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !info.is_empty() && info.contains('|') {
return Some(info);
let timeout = std::time::Duration::from_secs(5);
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
if status.success() {
let mut output = String::new();
if let Some(mut stdout) = child.stdout.take() {
use std::io::Read;
stdout.read_to_string(&mut output).ok()?;
}
let info = output.trim().to_string();
if !info.is_empty() && info.contains('|') {
return Some(info);
}
}
return None;
}
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
log::warn!("Timed out reading update info from {}", path.display());
return None;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(_) => return None,
}
}
None
}
// -- GitHub/GitLab API types for JSON deserialization --
#[derive(Deserialize)]
#[allow(dead_code)]
struct GhRelease {
tag_name: String,
name: Option<String>,
@@ -406,7 +430,6 @@ struct GhAsset {
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct GlRelease {
tag_name: String,
name: Option<String>,
@@ -492,6 +515,12 @@ fn check_github_release(
let release: GhRelease = response.body_mut().read_json().ok()?;
log::info!(
"GitHub release: tag={}, name={:?}",
release.tag_name,
release.name.as_deref().unwrap_or("(none)"),
);
let latest_version = clean_version(&release.tag_name);
// Find matching asset using glob-like pattern
@@ -549,6 +578,12 @@ fn check_gitlab_release(
let release: GlRelease = response.body_mut().read_json().ok()?;
log::info!(
"GitLab release: tag={}, name={:?}",
release.tag_name,
release.name.as_deref().unwrap_or("(none)"),
);
let latest_version = clean_version(&release.tag_name);
let download_url = release.assets.and_then(|assets| {
@@ -669,18 +704,24 @@ fn glob_match(pattern: &str, text: &str) -> bool {
// Last part must match at the end (unless pattern ends with *)
let last = parts[parts.len() - 1];
if !last.is_empty() {
let end_limit = if !last.is_empty() {
if !text.ends_with(last) {
return false;
}
}
text.len() - last.len()
} else {
text.len()
};
// Middle parts must appear in order
// Middle parts must appear in order within the allowed range
for part in &parts[1..parts.len() - 1] {
if part.is_empty() {
continue;
}
if let Some(found) = text[pos..].find(part) {
if pos >= end_limit {
return false;
}
if let Some(found) = text[pos..end_limit].find(part) {
pos += found + part.len();
} else {
return false;
@@ -691,7 +732,7 @@ fn glob_match(pattern: &str, text: &str) -> bool {
}
/// Clean a version string - strip leading 'v' or 'V' prefix.
fn clean_version(version: &str) -> String {
pub(crate) fn clean_version(version: &str) -> String {
let v = version.trim();
v.strip_prefix('v')
.or_else(|| v.strip_prefix('V'))
@@ -949,24 +990,25 @@ fn download_file(
/// Verify that a file is a valid AppImage (has ELF header + AppImage magic bytes).
fn verify_appimage(path: &Path) -> bool {
if let Ok(data) = fs::read(path) {
if data.len() < 12 {
return false;
}
// Check ELF magic
if &data[0..4] != b"\x7FELF" {
return false;
}
// Check AppImage Type 2 magic at offset 8: AI\x02
if data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x02 {
return true;
}
// Check AppImage Type 1 magic at offset 8: AI\x01
if data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x01 {
return true;
}
use std::io::Read;
let mut file = match fs::File::open(path) {
Ok(f) => f,
Err(_) => return false,
};
let mut header = [0u8; 12];
if file.read_exact(&mut header).is_err() {
return false;
}
false
// Check ELF magic
if &header[0..4] != b"\x7FELF" {
return false;
}
// Check AppImage Type 2 magic at offset 8: AI\x02
if header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02 {
return true;
}
// Check AppImage Type 1 magic at offset 8: AI\x01
header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x01
}
/// Perform an update using the best available method.