use anyhow::Result;
pub fn validate_executable_string(
value: &str,
option_name: &str,
line_number: usize,
) -> Result<()> {
const DANGEROUS_CHARS: &[char] = &[
';', '&', '|', '`', '$', '>', '<', '\n', '\r', '\0', ];
if let Some(dangerous_char) = value.chars().find(|c| DANGEROUS_CHARS.contains(c)) {
anyhow::bail!(
"Security violation: {option_name} contains dangerous character '{dangerous_char}' at line {line_number}. \
This could enable command injection attacks."
);
}
if value.contains("$(") || value.contains("${") {
anyhow::bail!(
"Security violation: {option_name} contains command substitution pattern at line {line_number}. \
This could enable command injection attacks."
);
}
validate_quotes(value, option_name, line_number)?;
if option_name == "ControlPath" {
validate_control_path_specific(value, line_number)?;
}
if option_name == "ProxyCommand" {
validate_proxy_command(value, line_number)?;
}
if option_name == "KnownHostsCommand" || option_name == "LocalCommand" {
validate_local_executable_command(value, option_name, line_number)?;
}
Ok(())
}
fn validate_quotes(value: &str, option_name: &str, line_number: usize) -> Result<()> {
let mut quote_count = 0;
let chars: Vec<char> = value.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '"' {
let mut backslash_count = 0;
let mut pos = i;
while pos > 0 {
pos -= 1;
if chars[pos] == '\\' {
backslash_count += 1;
} else {
break;
}
}
if backslash_count % 2 == 0 {
quote_count += 1;
}
}
}
if quote_count % 2 != 0 {
anyhow::bail!(
"Security violation: {option_name} contains unmatched quote at line {line_number}. \
This could enable command injection attacks."
);
}
Ok(())
}
fn validate_control_path_specific(value: &str, line_number: usize) -> Result<()> {
if value.trim_start().starts_with('-') {
anyhow::bail!(
"Security violation: ControlPath starts with '-' at line {line_number}. \
This could be interpreted as a command flag."
);
}
let chars: Vec<char> = value.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' && i + 1 < chars.len() {
let next_char = chars[i + 1];
match next_char {
'h' | 'p' | 'r' | 'u' | 'L' | 'l' | 'n' | 'd' | '%' => {
i += 2; }
_ => {
anyhow::bail!(
"Security violation: ControlPath contains unknown substitution pattern '%{next_char}' at line {line_number}. \
Only %h, %p, %r, %u, %L, %l, %n, %d, and %% are allowed."
);
}
}
} else {
i += 1;
}
}
Ok(())
}
fn validate_proxy_command(value: &str, line_number: usize) -> Result<()> {
if value == "none" {
return Ok(());
}
let trimmed = value.trim();
if trimmed.starts_with("bash ")
|| trimmed.starts_with("sh ")
|| trimmed.starts_with("/bin/")
|| trimmed.starts_with("python ")
|| trimmed.starts_with("perl ")
|| trimmed.starts_with("ruby ")
{
tracing::warn!(
"ProxyCommand at line {} uses potentially risky executable '{}'. \
Ensure this is intentional and from a trusted source.",
line_number,
trimmed.split_whitespace().next().unwrap_or("")
);
}
let lower_value = value.to_lowercase();
if lower_value.contains("curl ")
|| lower_value.contains("wget ")
|| lower_value.contains("nc ")
|| lower_value.contains("netcat ")
|| lower_value.contains("rm ")
|| lower_value.contains("dd ")
|| lower_value.contains("cat /")
{
anyhow::bail!(
"Security violation: ProxyCommand contains suspicious command pattern at line {line_number}. \
Commands like curl, wget, nc, rm, dd are not typical for SSH proxying."
);
}
Ok(())
}
fn validate_local_executable_command(
value: &str,
option_name: &str,
line_number: usize,
) -> Result<()> {
let trimmed = value.trim();
let lower_value = value.to_lowercase();
if lower_value.contains("curl ")
|| lower_value.contains("wget ")
|| lower_value.contains(" nc ") || lower_value.starts_with("nc ")
|| lower_value.contains("netcat ")
|| lower_value.contains("socat ")
|| lower_value.contains("telnet ")
{
anyhow::bail!(
"Security violation: {option_name} contains network command at line {line_number}. \
Commands like curl, wget, nc could be used for data exfiltration or downloading malicious content."
);
}
if lower_value.contains("rm ")
|| lower_value.contains("dd ")
|| lower_value.contains("mkfs")
|| lower_value.contains("format ")
{
anyhow::bail!(
"Security violation: {option_name} contains potentially destructive command at line {line_number}. \
Commands like rm, dd, mkfs could cause data loss."
);
}
if trimmed.starts_with("bash ")
|| trimmed.starts_with("sh ")
|| trimmed.starts_with("/bin/bash")
|| trimmed.starts_with("/bin/sh")
|| trimmed.starts_with("python ")
|| trimmed.starts_with("perl ")
|| trimmed.starts_with("ruby ")
{
tracing::warn!(
"{} at line {} invokes a shell or interpreter '{}'. \
Ensure this is intentional and from a trusted source.",
option_name,
line_number,
trimmed.split_whitespace().next().unwrap_or("")
);
}
Ok(())
}
pub fn validate_control_path(path: &str, line_number: usize) -> Result<()> {
if path == "none" {
return Ok(());
}
const DANGEROUS_CHARS: &[char] = &[
';', '&', '|', '`', '>', '<', '\n', '\r', '\0', ];
if let Some(dangerous_char) = path.chars().find(|c| DANGEROUS_CHARS.contains(c)) {
anyhow::bail!(
"Security violation: ControlPath contains dangerous character '{dangerous_char}' at line {line_number}. \
This could enable command injection attacks."
);
}
if path.contains("$(") {
anyhow::bail!(
"Security violation: ControlPath contains command substitution pattern at line {line_number}. \
This could enable command injection attacks."
);
}
if path.trim_start().starts_with('-') {
anyhow::bail!(
"Security violation: ControlPath starts with '-' at line {line_number}. \
This could be interpreted as a command flag."
);
}
let chars: Vec<char> = path.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' && i + 1 < chars.len() {
let next_char = chars[i + 1];
match next_char {
'h' | 'p' | 'r' | 'u' | 'L' | 'l' | 'n' | 'd' | '%' => {
i += 2; }
_ => {
anyhow::bail!(
"Security violation: ControlPath contains unknown substitution pattern '%{next_char}' at line {line_number}. \
Only %h, %p, %r, %u, %L, %l, %n, %d, and %% are allowed."
);
}
}
} else {
i += 1;
}
}
Ok(())
}