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::process::Command;
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.
#[derive(Debug, Clone)]
@@ -167,8 +166,8 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
eprintln!("[+] Service started");
std::thread::sleep(std::time::Duration::from_secs(1));
let status = resolve_trusted_helper("systemctl").and_then(|command_path| {
Command::new(command_path)
let status = trusted_helper_command("systemctl").and_then(|mut command| {
command
.args(["is-active", "telemt.service"])
.output()
.ok()
@@ -334,11 +333,11 @@ weight = 10
}
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);
return;
};
match Command::new(command_path).args(args).output() {
match command.args(args).output() {
Ok(output) => {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
+3 -3
View File
@@ -6,7 +6,7 @@ use tokio::io::AsyncWriteExt;
use tokio::process::Command;
#[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);
@@ -133,13 +133,13 @@ impl FirewallCommandRunner for SystemCommandRunner {
}
#[cfg(unix)]
{
let Some(command_path) = resolve_trusted_helper(spec.binary) else {
let Some(command) = trusted_helper_command(spec.binary) else {
return Err(CommandError {
kind: CommandErrorKind::Missing,
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.env("LC_ALL", "C");
if spec.stdin.is_some() {
+5 -5
View File
@@ -1,7 +1,7 @@
use tokio::io::AsyncWriteExt;
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);
@@ -10,10 +10,10 @@ pub(super) async fn run_command(
args: &[&str],
stdin: Option<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"));
};
let mut command = Command::new(command_path);
let mut command = Command::from(command);
command.args(args);
if stdin.is_some() {
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> {
let Some(command_path) = resolve_trusted_helper(binary) else {
let Some(command) = trusted_helper_command(binary) else {
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);
let output = tokio::time::timeout(COMMAND_TIMEOUT, command.output())
.await
+36
View File
@@ -1,5 +1,7 @@
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
const TRUSTED_HELPER_DIRS: [&str; 4] = ["/usr/sbin", "/usr/bin", "/sbin", "/bin"];
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))
}
/// 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> {
let canonical = std::fs::canonicalize(candidate).ok()?;
let metadata = std::fs::metadata(&canonical).ok()?;
@@ -78,6 +93,27 @@ mod tests {
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]
fn writable_executable_is_not_trusted() {
let directory = tempfile::tempdir().unwrap();