Reconcile the redesign with the f7b114f privileged-command hardening

The redesign branch was cut before "Harden privileged command operands
against injection" landed on main; the rebase conflicts in users.rs,
power.rs, firewall.rs and capabilities/default.json were resolved to
keep the stricter side of each:

- users.rs: keep `chpasswd_input` / `valid_chpasswd_password` (validates
  the *password* for `:` / newline, which the redesign's inline checks
  missed) and `may_delete_user` (refuses root / the current user);
  fold in the redesign's `--` argv separators and GECOS control-char
  stripping on top.
- power.rs: keep `charge_threshold_write` (validates `which` ∈
  {start,end} and clamps) + `util::run_with_stdin` for the tee pipe,
  plus its test; take the redesign's brightness clamp.
- firewall.rs: keep the port-only `valid_firewall_rule` /
  `valid_rule_number` allowlist from f7b114f. NOTE: the redesign's
  Firewall UI hint advertises service names ("OpenSSH") which this
  validator rejects — either loosen it to `util::valid_cli_value` or
  drop the service-name hint. Left strict pending that call.
- capabilities/default.json: keep `opener:default` removed (nothing in
  the frontend or backend uses the opener plugin); add the four
  `core🪟*` perms the redesign's custom Titlebar needs.

Drops `util::valid_username` / `util::valid_cli_value` (added by the
redesign, now unused — every call site uses the stricter f7b114f local
validator) and the unused `use super::util` in firewall.rs.
This commit is contained in:
Breadway 2026-08-31 18:01:57 +08:00
parent 57898400d3
commit 3dfc9d56f3
2 changed files with 0 additions and 64 deletions

View file

@ -7,8 +7,6 @@
use serde::Serialize; use serde::Serialize;
use tokio::process::Command; use tokio::process::Command;
use super::util;
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct FirewallRule { pub struct FirewallRule {
number: String, number: String,

View file

@ -241,38 +241,6 @@ pub fn valid_printer_name(name: &str) -> bool {
.all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.')) .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.'))
} }
/// Linux account usernames: lowercase letters, digits, `_`, `-`, `.`;
/// must start with a lowercase letter (rejects a leading `-`, which would
/// be a flag injection into useradd/userdel); capped at useradd's 32-char MAX.
///
/// We deliberately do *not* accept a leading `@`/domain or spaces: account
/// creation here is a plain local user.
pub fn valid_username(name: &str) -> bool {
let bytes = name.as_bytes();
!bytes.is_empty()
&& bytes.len() <= 32
&& bytes[0].is_ascii_lowercase()
&& bytes
.iter()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(*b, b'_' | b'-' | b'.'))
}
/// Free-form text thrown at a CLI tool as a single argv element (firewall
/// rule string, etc). Blocks the two genuinely dangerous shapes — a leading
/// flag `-` and any control/whitespace injection — while still allowing
/// spaces, slashes, dots, colons etc that ufw rules legitimately use.
pub fn valid_cli_value(name: &str) -> bool {
let t = name.trim();
!t.is_empty()
&& t.len() <= 256
&& !t.starts_with('-')
&& !t.contains('\n')
&& !t.contains('\r')
&& !t.contains('\0')
&& !t.contains(';')
&& t.bytes().all(|b| !(0..=31).contains(&b))
}
/// ufw / snapper numeric id — digits only. /// ufw / snapper numeric id — digits only.
pub fn valid_number_id(num: &str) -> bool { pub fn valid_number_id(num: &str) -> bool {
!num.is_empty() && num.len() <= 12 && num.bytes().all(|b| b.is_ascii_digit()) !num.is_empty() && num.len() <= 12 && num.bytes().all(|b| b.is_ascii_digit())
@ -322,36 +290,6 @@ mod tests {
assert!(!valid_printer_name("-d")); assert!(!valid_printer_name("-d"));
} }
#[test]
fn username_accepts_normal_accounts() {
assert!(valid_username("alice"));
assert!(valid_username("bob_2"));
assert!(valid_username("john.doe"));
assert!(valid_username("a"));
}
#[test]
fn username_rejects_flags_and_injection() {
assert!(!valid_username(""));
assert!(!valid_username("-r")); // system-account flag into useradd
assert!(!valid_username("--system"));
assert!(!valid_username("a\nb"));
assert!(!valid_username("foo bar"));
assert!(!valid_username("UPPER")); // must start lowercase
assert!(!valid_username(&"a".repeat(33)));
}
#[test]
fn cli_value_rejects_flags_and_controls() {
assert!(valid_cli_value("80/tcp"));
assert!(valid_cli_value("from 192.168.1.0/24 to any port 53"));
assert!(!valid_cli_value("--all"));
assert!(!valid_cli_value("-n"));
assert!(!valid_cli_value("a\nb"));
assert!(!valid_cli_value("a;rm"));
assert!(!valid_cli_value(""));
}
#[test] #[test]
fn number_id_is_digits_only() { fn number_id_is_digits_only() {
assert!(valid_number_id("42")); assert!(valid_number_id("42"));