概念定义
进程以真实权限运行命令。命令行一旦按字符串拼装,输入就不是数据而是语法——shell 会解析它,攻击者由此获得 shell 的一切能力:命令分隔、重定向、变量展开。
工作机制
file.png; curl attacker -o x 这类文件名在朴素插值下变成两条命令。安全做法是传 argv 数组走 execve 且不经 shell:每个参数都是字面字符串,元字符全部失效。确实绕不开 shell 时,才退而使用白名单校验与严格引用规范,并经评审。
使用场景
审计一切可由请求数据触达的 spawn、exec、system、反引号调用,包括经任务队列与模板渲染的间接路径。图像、压缩、文件任务大多有语言原生库,优先用库别 shell。
常见误区
对输入做引号转义不是修复——shell 解释的远不止引号;黑名单封“危险字符”也终会被编码与遗漏击穿。没有 shell 仍有参数级注入:作为单个 argv 元素的 –output=/etc/x 照样改语义,应校验格式并在工具支持时把用户数据放在双横线之后。日志要记录真实 argv 而不是重组的 shell 串。
代码示例
import subprocess
from dataclasses import dataclass
from typing import Mapping, Optional, Sequence
class UnsafeCommandError(ValueError):
"""Raised when a command would require shell interpretation."""
@dataclass(frozen=True)
class CommandResult:
argv: Sequence[str]
returncode: int
stdout: str
stderr: str
def run_command(
binary: str,
args: Sequence[str],
*,
timeout_s: float = 10.0,
env: Optional[Mapping[str, str]] = None,
use_shell: bool = False,
) -> CommandResult:
"""Run binary with an argv list and no shell; return a structured result."""
if use_shell:
raise UnsafeCommandError("shell mode is refused; pass an argv list instead")
if not binary:
raise ValueError("binary must be a non-empty path")
argv = [binary, *[str(a) for a in args]]
try:
completed = subprocess.run(
argv,
shell=False,
capture_output=True,
text=True,
timeout=timeout_s,
env=dict(env) if env is not None else None,
)
except subprocess.TimeoutExpired as exc:
raise TimeoutError("command timed out after %ss" % timeout_s) from exc
except FileNotFoundError as exc:
raise FileNotFoundError("binary not found: %s" % binary) from exc
return CommandResult(
argv=argv,
returncode=completed.returncode,
stdout=completed.stdout,
stderr=completed.stderr,
)
const encoder = new TextEncoder();
// Constant-time comparison for secrets and signatures.
export function constantTimeEqual(left, right) {
const a = encoder.encode(String(left));
const b = encoder.encode(String(right));
const length = Math.max(a.length, b.length);
let difference = a.length ^ b.length;
for (let index = 0; index < length; index += 1) {
difference |= (a[index] ?? 0) ^ (b[index] ?? 0);
}
return difference === 0;
}
export function constantTimeEqualBytes(left, right) {
if (!(left instanceof Uint8Array) || !(right instanceof Uint8Array)) {
throw new TypeError("both arguments must be Uint8Array values");
}
const length = Math.max(left.length, right.length);
let difference = left.length ^ right.length;
for (let index = 0; index < length; index += 1) {
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
}
return difference === 0;
}