From d7065dcb5987ca2a1598fc1800782cd583c6a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ayhan=20Yal=C3=A7=C4=B1nsoy?= Date: Wed, 3 Jun 2026 13:39:34 +0300 Subject: [PATCH] refactor: replace shell-based password handling with secure stdin piping for LUKS and GRUB operations --- Cargo.toml | 2 +- locales/en.toml | 3 +- locales/tr.toml | 3 +- src/autoinstall/mod.rs | 32 ++++++ src/installer.rs | 4 + src/jobs/mod.rs | 215 +++++++++++++++++++++++++++++++++-------- src/main.rs | 3 + src/steps/users.rs | 23 ++++- 8 files changed, 239 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ce22f52..abc32cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yali-rs" version = "4.0.0" -edition = "2021" +edition = "2024" [dependencies] eframe = "0.27" diff --git a/locales/en.toml b/locales/en.toml index 3f31d5d..c30c123 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -281,7 +281,7 @@ admin_title = "Root Password" admin_description = "Set a password for the root (administrator) account. Leave empty to disable direct root login." admin_password_label = "Root password:" admin_password_confirm_label = "Confirm root password:" -admin_password_short = "Root password must be at least 4 characters." +admin_password_short = "Root password must be at least 8 characters." # ── Date/Time ────────────────────────────────────────────── datetime = "Date & Time" @@ -532,3 +532,4 @@ lvm_lv_size = "Size (MB, 0=remaining):" lvm_lv_action = "Action" +oem_change_password_warning = "OEM mode: You must change the default password before continuing." diff --git a/locales/tr.toml b/locales/tr.toml index 1645185..e51e863 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -293,7 +293,7 @@ admin_title = "Root Parolası" admin_description = "Root (yönetici) hesabı için bir parola belirleyin. Boş bırakılırsa doğrudan root girişi devre dışı kalır." admin_password_label = "Root parolası:" admin_password_confirm_label = "Root parolası (tekrar):" -admin_password_short = "Root parolası en az 4 karakter olmalıdır." +admin_password_short = "Root parolası en az 8 karakter olmalıdır." # ── Tarih & Saat ──────────────────────────────────────────── datetime = "Tarih & Saat" @@ -539,3 +539,4 @@ xfce_thunar_description = "Toplu yeniden adlandırma ve eklentiler ile hızlı v xfce_settings_title = "XFCE Ayarları" xfce_settings_description = "Masaüstü özelleştirmesi için kapsamlı ayar yöneticisi." +oem_change_password_warning = "OEM modu: Devam etmeden önce varsayılan şifreyi değiştirmelisiniz." diff --git a/src/autoinstall/mod.rs b/src/autoinstall/mod.rs index d54e8ab..f4d67de 100644 --- a/src/autoinstall/mod.rs +++ b/src/autoinstall/mod.rs @@ -66,6 +66,38 @@ pub fn load(path: &std::path::Path) -> Result { pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState) { use crate::installer::{DiskInfo, PartitionPlan, PartitionTableType}; + // --- Giriş doğrulama --- + // Disk yolu: yalnızca /dev/ ile başlayan, alfanumerik + /._- içeren değerler + let disk = &af.install.disk; + if !disk.starts_with("/dev/") + || disk.contains("..") + || !disk.chars().all(|c| c.is_alphanumeric() || "/_.-".contains(c)) + { + eprintln!("autoinstall: geçersiz disk yolu reddedildi: '{}'", disk); + return; + } + + // Timezone doğrulama + let tz = &af.install.timezone; + if tz.is_empty() + || tz.contains("..") + || !tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c)) + { + eprintln!("autoinstall: geçersiz timezone reddedildi: '{}'", tz); + return; + } + + // Locale doğrulama + let locale = &af.install.locale; + if locale.is_empty() + || locale.contains("..") + || locale.contains('/') + || !locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c)) + { + eprintln!("autoinstall: geçersiz locale reddedildi: '{}'", locale); + return; + } + // Temel alanlar state.timezone = af.install.timezone.clone(); state.keyboard_layout = af.install.keyboard.clone(); diff --git a/src/installer.rs b/src/installer.rs index f34e297..300f575 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -232,6 +232,9 @@ pub struct GlobalState { #[allow(dead_code)] pub luks_password: String, pub oem_mode: bool, + /// OEM modunda: kullanıcı varsayılan şifreyi değiştirdi mi? + /// true olmadan kurulum ilerleyemez. + pub oem_password_changed: bool, pub selected_package_groups: Vec, /// branding.toml [slides.icons] tablosundan yüklenen ikon → dosya URI eşlemesi. /// Slideshow bu map üzerinden çalışma zamanında görsel yollarını çözümler. @@ -290,6 +293,7 @@ impl Default for GlobalState { encrypt_root: false, luks_password: String::new(), oem_mode: false, + oem_password_changed: false, selected_package_groups: Vec::new(), display_manager: "sddm".to_string(), autologin: true, diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index f9ee48d..e22f510 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -220,12 +220,32 @@ impl Job for UnmountBindJob { "sys", "proc", ]; + let mut errors: Vec = Vec::new(); for subdir in &subdirs { let target = format!("{}/{}", mt, subdir); ui.log(format!("umount {}", target)); - // Hata görmezden gel; bazıları zaten unmount olmuş olabilir - let _ = tokio::process::Command::new("umount") - .arg("-lf").arg(&target).status().await; + match tokio::process::Command::new("umount") + .arg("-lf").arg(&target).status().await + { + Ok(status) if status.success() => {} + Ok(status) => { + // "not mounted" veya "no such file" hataları beklenen durumlar — + // bunları uyarı olarak logla ama kritik hata sayma. + ui.log(format!("⚠ umount uyarısı (kritik değil, kod {}): {}", status, target)); + } + Err(e) => { + // Komut çalıştırılamadı — bu gerçek bir sorun. + let msg = format!("umount komutu başlatılamadı ({}): {}", target, e); + ui.log(format!("✗ {}", msg)); + errors.push(msg); + } + } + } + if !errors.is_empty() { + return Err(format!( + "Bind mount temizleme hatası — bazı sanal dosya sistemleri bağlı kalmış olabilir:\n{}", + errors.join("\n") + )); } Ok(()) } @@ -255,11 +275,14 @@ impl Job for GenerateInitramfsJob { let _ = tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" lvm \"\n").await; // mkinitcpio: add lvm2 hook + // Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir. let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount); if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() { if let Ok(content) = tokio::fs::read_to_string(&mkinitcpio_conf).await { if content.contains("HOOKS=") && !content.contains("lvm2") { - let new_content = content.replace("HOOKS=\"", "HOOKS=\"lvm2 "); + let new_content = content + .replace("HOOKS=\"", "HOOKS=\"lvm2 ") + .replace("HOOKS=(", "HOOKS=(lvm2 "); let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await; } } @@ -279,12 +302,15 @@ impl Job for GenerateInitramfsJob { .await.map_err(|e| format!("dracut luks conf yazılamadı: {}", e))?; // mkinitcpio: add encrypt hook + // Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir. let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount); if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() { let content = tokio::fs::read_to_string(&mkinitcpio_conf) .await.unwrap_or_default(); if content.contains("HOOKS=") && !content.contains("encrypt") { - let new_content = content.replace("HOOKS=\"", "HOOKS=\"encrypt "); + let new_content = content + .replace("HOOKS=\"", "HOOKS=\"encrypt ") + .replace("HOOKS=(", "HOOKS=(encrypt "); let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await; } } @@ -331,18 +357,49 @@ impl Job for InstallGrubJob { let grub_d = format!("{}/etc/grub.d", self.mount); let _ = tokio::fs::create_dir_all(&grub_d).await; - // GRUB şifresi (varsa) — bcrypt hash ile /etc/grub.d/40_custom yazılır + // GRUB şifresi (varsa) — grub-mkpasswd-pbkdf2 ile hash üretilir, + // düz metin asla diske yazılmaz. if !self.password.is_empty() { - ui.log("GRUB şifresi yapılandırılıyor…"); + ui.log("GRUB şifresi PBKDF2 ile hashleniyor…"); + + // grub-mkpasswd-pbkdf2 şifreyi stdin'den iki kez okur (şifre + doğrulama) + use tokio::io::AsyncWriteExt; + let pw_input = format!("{}\n{}\n", self.password, self.password); + let mut child = tokio::process::Command::new("grub-mkpasswd-pbkdf2") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + .map_err(|e| format!("grub-mkpasswd-pbkdf2 başlatılamadı: {}", e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(pw_input.as_bytes()).await + .map_err(|e| format!("grub-mkpasswd-pbkdf2 stdin hatası: {}", e))?; + } + + let output = child.wait_with_output().await + .map_err(|e| format!("grub-mkpasswd-pbkdf2 bekleme hatası: {}", e))?; + + // Çıktıdan "grub.pbkdf2.sha512...." token'ını ayrıştır + let stdout = String::from_utf8_lossy(&output.stdout); + let hash = stdout + .lines() + .find_map(|l| { + let pos = l.find("grub.pbkdf2")?; + Some(l[pos..].trim().to_string()) + }) + .ok_or("grub-mkpasswd-pbkdf2 çıktısında hash bulunamadı")?; + let custom_grub = format!( - "set superusers=\"root\"\npassword_pbkdf2 root {} \n", - self.password + "#!/bin/sh\nset superusers=\"root\"\npassword_pbkdf2 root {}\n", + hash ); let pw_path = format!("{}/etc/grub.d/40_custom", self.mount); tokio::fs::write(&pw_path, custom_grub) .await .map_err(|e| format!("GRUB şifre dosyası yazılamadı: {}", e))?; run_cmd("chmod", &["+x", &pw_path]).await?; + ui.log("GRUB şifresi PBKDF2 hash olarak yazıldı."); } // GRUB timeout ayarı @@ -552,6 +609,79 @@ async fn run_chroot(mount: &str, cmd: &[&str]) -> Result<(), String> { } } +/// LUKS bölümünü güvenli şekilde formatlar. +/// +/// Şifre, `sh -c` + string interpolasyon yerine doğrudan `stdin` pipe'ı +/// üzerinden iletilir; böylece shell injection saldırıları önlenir. +async fn run_luks_format(device: &str, password: &str) -> Result<(), String> { + if DEMO_MODE.load(Ordering::Relaxed) { + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + return Ok(()); + } + + use tokio::io::AsyncWriteExt; + + let mut child = tokio::process::Command::new("cryptsetup") + .args([ + "luksFormat", "-q", "--type", "luks2", + "--key-file", "-", + device, + ]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| format!("cryptsetup luksFormat başlatılamadı: {}", e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(password.as_bytes()).await + .map_err(|e| format!("cryptsetup stdin yazma hatası: {}", e))?; + } + + let output = child.wait_with_output().await + .map_err(|e| format!("cryptsetup bekleme hatası: {}", e))?; + + if output.status.success() { + Ok(()) + } else { + Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) + } +} + +/// LUKS bölümünü güvenli şekilde açar (open). +/// +/// Şifre, shell interpolasyon yerine `stdin` pipe'ı üzerinden iletilir. +async fn run_luks_open(device: &str, name: &str, password: &str) -> Result<(), String> { + if DEMO_MODE.load(Ordering::Relaxed) { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + return Ok(()); + } + + use tokio::io::AsyncWriteExt; + + let mut child = tokio::process::Command::new("cryptsetup") + .args(["open", "--key-file", "-", device, name]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| format!("cryptsetup open başlatılamadı: {}", e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(password.as_bytes()).await + .map_err(|e| format!("cryptsetup open stdin yazma hatası: {}", e))?; + } + + let output = child.wait_with_output().await + .map_err(|e| format!("cryptsetup open bekleme hatası: {}", e))?; + + if output.status.success() { + Ok(()) + } else { + Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) + } +} + /// Bölümlendirme öncesi diski hazırlar: /// mevcut bağlamaları ve swap'ı ayırır, ardından kernel'e yeni /// bölüm tablosunu duyurarak aygıt düğümlerinin hazır olmasını bekler. @@ -791,25 +921,17 @@ impl Job for PartitionJob { run_cmd("swapon", &[&swap_part]).await?; // LUKS şifreleme (kök bölümü) + // Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden + // iletilir; `sh -c` + string interpolasyon kullanılmaz. let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() { - let luks_name = "cryptroot".to_string(); + let luks_name = "cryptroot"; ui.log(format!("cryptsetup luksFormat {}…", root_part)); - let _ = run_cmd("cryptsetup", &[ - "luksFormat", "-q", "--type", "luks2", &root_part, - "--key-file", "-", - ]).await; - // Burada batch modda çalıştığımız için şifreyi pipe ile veremeyiz; - // bunun yerine echo -n şifre | cryptsetup ile çalışır - let password_input = format!("echo -n '{}' | cryptsetup luksFormat -q --type luks2 {} --key-file=-", self.plan.luks_password, root_part); - let _ = tokio::process::Command::new("sh") - .args(["-c", &password_input]) - .output().await; + run_luks_format(&root_part, &self.plan.luks_password).await + .map_err(|e| format!("LUKS format hatası: {}", e))?; ui.log(format!("cryptsetup open {} {}…", root_part, luks_name)); - let open_cmd = format!("echo -n '{}' | cryptsetup open {} {}", self.plan.luks_password, root_part, luks_name); - let _ = tokio::process::Command::new("sh") - .args(["-c", &open_cmd]) - .output().await; + run_luks_open(&root_part, luks_name, &self.plan.luks_password).await + .map_err(|e| format!("LUKS open hatası: {}", e))?; format!("/dev/mapper/{}", luks_name) } else { @@ -1289,6 +1411,17 @@ impl Job for ConfigureLocaleJob { fn name(&self) -> &str { "job_locale" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + // Locale değeri doğrulama: yalnızca alfanumerik, '.', '_', '-', '@' + // karakterlerine izin ver. Yol manipülasyonu ve injection önlenir. + let locale_valid = !self.locale.is_empty() + && !self.locale.contains("..") + && !self.locale.contains('/') + && !self.locale.contains('\n') + && self.locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c)); + if !locale_valid { + return Err(format!("Geçersiz locale değeri: '{}'", self.locale)); + } + let etc_dir = format!("{}/etc", self.mount); let _ = tokio::fs::create_dir_all(&etc_dir).await; let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await; @@ -1331,9 +1464,9 @@ impl Job for ConfigureLocaleJob { .await .map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?; - // locale-gen çalıştır + // locale-gen çalıştır — kurulum sonrası locale desteği için zorunlu ui.log("locale-gen çalıştırılıyor…"); - //run_chroot(&self.mount, &["locale-gen"]).await?; + run_chroot(&self.mount, &["locale-gen"]).await?; Ok(()) } @@ -1350,6 +1483,16 @@ impl Job for ConfigureTimezoneJob { fn name(&self) -> &str { "job_timezone" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + // Path traversal koruması: timezone değeri yalnızca alfanumerik, + // '/', '_', '-', '+' karakterlerine izin verir; ".." asla geçemez. + let tz = &self.timezone; + let tz_valid = !tz.is_empty() + && !tz.contains("..") + && tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c)); + if !tz_valid { + return Err(format!("Geçersiz timezone değeri: '{}'", tz)); + } + // Sembolik link: /etc/localtime → /usr/share/zoneinfo/ let link_target = format!("/usr/share/zoneinfo/{}", self.timezone); let link_path = format!("{}/etc/localtime", self.mount); @@ -1775,6 +1918,8 @@ impl Job for CustomPartitionJob { let path = &part.device; // LUKS şifreleme (kök veya isteğe bağlı bölümler) + // Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden + // iletilir; `sh -c` + string interpolasyon kullanılmaz. let fs_device = if part.encrypt && !part.luks_password.is_empty() { let luks_name = if part.luks_name.is_empty() { format!("crypt_{}", part.mountpoint.replace("/", "_")) @@ -1784,22 +1929,12 @@ impl Job for CustomPartitionJob { let luks_name = luks_name.trim_start_matches('_').to_string(); ui.log(format!("cryptsetup luksFormat {}…", path)); - let format_cmd = format!( - "echo -n '{}' | cryptsetup luksFormat -q --type luks2 {} --key-file=-", - part.luks_password, path - ); - let _ = tokio::process::Command::new("sh") - .args(["-c", &format_cmd]) - .output().await; + run_luks_format(path, &part.luks_password).await + .map_err(|e| format!("LUKS format hatası ({}): {}", path, e))?; ui.log(format!("cryptsetup open {} {}…", path, luks_name)); - let open_cmd = format!( - "echo -n '{}' | cryptsetup open {} {}", - part.luks_password, path, luks_name - ); - let _ = tokio::process::Command::new("sh") - .args(["-c", &open_cmd]) - .output().await; + run_luks_open(path, &luks_name, &part.luks_password).await + .map_err(|e| format!("LUKS open hatası ({}): {}", path, e))?; format!("/dev/mapper/{}", luks_name) } else { diff --git a/src/main.rs b/src/main.rs index 4611f5d..84ad9d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -245,6 +245,9 @@ impl Default for YaliApp { if !branding.oem.default_password.is_empty() { app.state.password = branding.oem.default_password.clone(); app.state.password_confirm = branding.oem.default_password.clone(); + // OEM varsayılan şifresi yüklendi; kullanıcı değiştirene kadar + // ilerleme engellenir. + app.state.oem_password_changed = false; } // OEM ön tanımlı disk planı varsa? } diff --git a/src/steps/users.rs b/src/steps/users.rs index bcfad76..939caab 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -193,12 +193,24 @@ impl InstallerStep for UsersStep { // Şifre ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text())); + // OEM modunda varsayılan şifre uyarısı göster + if state.oem_mode && !state.oem_password_changed { + ui.colored_label( + theme::c_error(), + t!("oem_change_password_warning"), + ); + } ui.horizontal(|ui| { + let before = state.password.clone(); if self.show_password { ui.text_edit_singleline(&mut state.password); } else { ui.add(egui::TextEdit::singleline(&mut state.password).password(true)); } + // Kullanıcı şifreyi değiştirdiyse OEM bayrağını kaldır + if state.oem_mode && state.password != before { + state.oem_password_changed = true; + } if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() { self.show_password = !self.show_password; } @@ -280,7 +292,7 @@ impl InstallerStep for UsersStep { if ui.small_button(if self.show_root_password { "🙈" } else { "👁" }).clicked() { self.show_root_password = !self.show_root_password; } - if !state.root_password.is_empty() && state.root_password.len() < 4 { + if !state.root_password.is_empty() && state.root_password.len() < 8 { ui.colored_label(theme::c_error(), t!("admin_password_short")); } }); @@ -321,9 +333,14 @@ impl InstallerStep for UsersStep { } fn is_complete(&self, state: &GlobalState) -> bool { + // OEM modunda varsayılan şifre değiştirilmeden ilerlenmez. + if state.oem_mode && !state.oem_password_changed { + return false; + } + let user_ok = is_valid_username(&state.username) && is_valid_hostname(&state.hostname) - && state.password.len() >= 4 + && state.password.len() >= 8 && state.password == state.password_confirm; if self.use_same_for_admin { @@ -331,7 +348,7 @@ impl InstallerStep for UsersStep { } else { user_ok && ( state.root_password.is_empty() - || (state.root_password.len() >= 4 && state.root_password == state.root_password_confirm) + || (state.root_password.len() >= 8 && state.root_password == state.root_password_confirm) ) } }