Compare commits

...

1 Commits

Author SHA1 Message Date
Alexey 226f9443e1 Trusted Helper Argv0 for Multi-call Firewall Binaries
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-09-28 05:48:43 +03:00
4 changed files with 49 additions and 14 deletions
+5 -6
View File
@@ -1,9 +1,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use rand::RngExt; use rand::RngExt;
use crate::util::trusted_command::resolve_trusted_helper; use crate::util::trusted_command::trusted_helper_command;
/// Options for the fire-and-forget init command. /// Options for the fire-and-forget init command.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -167,8 +166,8 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
eprintln!("[+] Service started"); eprintln!("[+] Service started");
std::thread::sleep(std::time::Duration::from_secs(1)); std::thread::sleep(std::time::Duration::from_secs(1));
let status = resolve_trusted_helper("systemctl").and_then(|command_path| { let status = trusted_helper_command("systemctl").and_then(|mut command| {
Command::new(command_path) command
.args(["is-active", "telemt.service"]) .args(["is-active", "telemt.service"])
.output() .output()
.ok() .ok()
@@ -334,11 +333,11 @@ weight = 10
} }
fn run_cmd(cmd: &str, args: &[&str]) { fn run_cmd(cmd: &str, args: &[&str]) {
let Some(command_path) = resolve_trusted_helper(cmd) else { let Some(mut command) = trusted_helper_command(cmd) else {
eprintln!("[!] Refusing unavailable or untrusted command: {}", cmd); eprintln!("[!] Refusing unavailable or untrusted command: {}", cmd);
return; return;
}; };
match Command::new(command_path).args(args).output() { match command.args(args).output() {
Ok(output) => { Ok(output) => {
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
+3 -3
View File
@@ -6,7 +6,7 @@ use tokio::io::AsyncWriteExt;
use tokio::process::Command; use tokio::process::Command;
#[cfg(unix)] #[cfg(unix)]
use crate::util::trusted_command::resolve_trusted_helper; use crate::util::trusted_command::{resolve_trusted_helper, trusted_helper_command};
const COMMAND_TIMEOUT: Duration = Duration::from_secs(30); const COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
@@ -133,13 +133,13 @@ impl FirewallCommandRunner for SystemCommandRunner {
} }
#[cfg(unix)] #[cfg(unix)]
{ {
let Some(command_path) = resolve_trusted_helper(spec.binary) else { let Some(command) = trusted_helper_command(spec.binary) else {
return Err(CommandError { return Err(CommandError {
kind: CommandErrorKind::Missing, kind: CommandErrorKind::Missing,
message: format!("{} is not available", spec.binary), message: format!("{} is not available", spec.binary),
}); });
}; };
let mut command = Command::new(command_path); let mut command = Command::from(command);
command.args(&spec.args); command.args(&spec.args);
command.env("LC_ALL", "C"); command.env("LC_ALL", "C");
if spec.stdin.is_some() { if spec.stdin.is_some() {
+5 -5
View File
@@ -1,7 +1,7 @@
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::process::Command; use tokio::process::Command;
use crate::util::trusted_command::resolve_trusted_helper; use crate::util::trusted_command::trusted_helper_command;
const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
@@ -10,10 +10,10 @@ pub(super) async fn run_command(
args: &[&str], args: &[&str],
stdin: Option<String>, stdin: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
let Some(command_path) = resolve_trusted_helper(binary) else { let Some(command) = trusted_helper_command(binary) else {
return Err(format!("{binary} is not available")); return Err(format!("{binary} is not available"));
}; };
let mut command = Command::new(command_path); let mut command = Command::from(command);
command.args(args); command.args(args);
if stdin.is_some() { if stdin.is_some() {
command.stdin(std::process::Stdio::piped()); command.stdin(std::process::Stdio::piped());
@@ -52,10 +52,10 @@ pub(super) async fn run_command(
} }
pub(super) async fn run_command_stdout(binary: &str, args: &[&str]) -> Result<String, String> { pub(super) async fn run_command_stdout(binary: &str, args: &[&str]) -> Result<String, String> {
let Some(command_path) = resolve_trusted_helper(binary) else { let Some(command) = trusted_helper_command(binary) else {
return Err(format!("{binary} is not available")); return Err(format!("{binary} is not available"));
}; };
let mut command = Command::new(command_path); let mut command = Command::from(command);
command.args(args).kill_on_drop(true); command.args(args).kill_on_drop(true);
let output = tokio::time::timeout(COMMAND_TIMEOUT, command.output()) let output = tokio::time::timeout(COMMAND_TIMEOUT, command.output())
.await .await
+36
View File
@@ -1,5 +1,7 @@
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
const TRUSTED_HELPER_DIRS: [&str; 4] = ["/usr/sbin", "/usr/bin", "/sbin", "/bin"]; const TRUSTED_HELPER_DIRS: [&str; 4] = ["/usr/sbin", "/usr/bin", "/sbin", "/bin"];
const TRUSTED_HELPERS: [&str; 12] = [ const TRUSTED_HELPERS: [&str; 12] = [
@@ -29,6 +31,19 @@ pub(crate) fn resolve_trusted_helper(binary: &str) -> Option<PathBuf> {
.find_map(|candidate| trusted_executable(&candidate)) .find_map(|candidate| trusted_executable(&candidate))
} }
/// Builds a trusted helper command with its allowlisted invocation name.
pub(crate) fn trusted_helper_command(binary: &str) -> Option<Command> {
let command_path = resolve_trusted_helper(binary)?;
Some(command_for_resolved_helper(binary, command_path))
}
fn command_for_resolved_helper(binary: &str, command_path: PathBuf) -> Command {
let mut command = Command::new(command_path);
// Multi-call helpers dispatch from argv[0], which canonical path resolution discards.
command.arg0(binary);
command
}
fn trusted_executable(candidate: &Path) -> Option<PathBuf> { fn trusted_executable(candidate: &Path) -> Option<PathBuf> {
let canonical = std::fs::canonicalize(candidate).ok()?; let canonical = std::fs::canonicalize(candidate).ok()?;
let metadata = std::fs::metadata(&canonical).ok()?; let metadata = std::fs::metadata(&canonical).ok()?;
@@ -78,6 +93,27 @@ mod tests {
assert!(!TRUSTED_HELPERS.contains(&"iptables-restore-wrapper")); assert!(!TRUSTED_HELPERS.contains(&"iptables-restore-wrapper"));
} }
#[test]
fn trusted_multicall_command_preserves_logical_argv0() {
let command_path = std::fs::canonicalize("/bin/sh").unwrap();
for binary in [
"iptables",
"ip6tables",
"iptables-restore",
"ip6tables-restore",
] {
let mut command = command_for_resolved_helper(binary, command_path.clone());
assert_eq!(command.get_program(), command_path.as_os_str());
let output = command
.args(["-c", "printf '%s' \"$0\""])
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8(output.stdout).unwrap(), binary);
}
}
#[test] #[test]
fn writable_executable_is_not_trusted() { fn writable_executable_is_not_trusted() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();