ToolkitX
知识库工具箱

安全实践

SQL 注入、XSS、CSRF 防护

20min·进阶

01. SQL 注入防护

SQL 注入是 PHP 圈最臭名昭著的安全漏洞。原理很简单:用户输入的数据被直接拼进 SQL 语句,用户恶意输入就可以改变 SQL 语句的逻辑。防护方法就是永远用预处理语句加参数绑定,不管是 PDO 还是 MySQLi 都支持。用户输入在进入 SQL 之前就和技术参数分开了。
php
<?php

// 危险写法 - 绝不用
$name = $_GET["name"];
$result = $pdo->query("SELECT * FROM users WHERE name = '$name'");
// 用户输入 ' OR '1'='1 就完蛋了

// 安全写法 - 永远用预处理
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([":name" => $_GET["name"]]);

// 动态排序字段 - 用白名单
$allowed = ["id", "name", "created_at"];
$sort = in_array($_GET["sort"], $allowed) ? $_GET["sort"] : "id";
$result = $pdo->query("SELECT * FROM users ORDER BY {$sort}");
ORDER BY 和 GROUP BY 不能用占位符,只能先白名单校验再拼进去。

02. XSS 跨站脚本攻击

XSS 是攻击者往你的页面注入恶意 JavaScript 代码。防护核心:所有输出到 HTML 的数据都要转义。htmlspecialchars 把 < > & " ' 转成 HTML 实体。模板引擎(Blade、Twig)默认就转义,裸 PHP 里要手动调用。用户提交的富文本要做白名单过滤。
php
<?php

// 输出到 HTML 时必须转义
echo htmlspecialchars($_GET["name"], ENT_QUOTES, "UTF-8");

// JavaScript 上下文里的转义(用在 <script> 里)
$safeJson = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS);

// HTML 属性里的转义
$attr = htmlspecialchars($value, ENT_COMPAT);

// 设置安全的 Cookie
setcookie("session", $value, [
    "httponly" => true,   // JS 读不到
    "secure" => true,     // 只通过 HTTPS 传
    "samesite" => "Lax",  // 防 CSRF
]);

// CSP 响应头(在 PHP 里设置)
header("Content-Security-Policy: default-src 'self'");
设置 Cookie 的 httponly 标志后,即使有 XSS 漏洞攻击者也偷不到 Cookie。

03. CSRF 跨站请求伪造

CSRF 攻击利用了你已登录的身份,在你不知情的情况下发请求干坏事(比如转账)。防护方法:在每个表单加一个随机 token,提交时验证 token 对不对。Laravel 的 @csrf 和表单里的 csrf_field() 自动做了。AJAX 请求也要在 header 里带上 CSRF token。
php
<?php

// 生成 CSRF token
session_start();
if (empty($_SESSION["csrf_token"])) {
    $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}

// 表单里加隐藏字段
// <form method="POST">
//   <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
// </form>

// 验证
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    if (!hash_equals($_SESSION["csrf_token"], $_POST["csrf_token"] ?? "")) {
        die("CSRF token 验证失败");
    }
}

// AJAX 验证(Laravel 方式)
// headers: { "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content }
用 hash_equals 比较 token 而不是 == ,因为 hash_equals 是恒定时间比较不会受到时序攻击。

04. 密码安全

存密码绝对不能存明文!也不能用 MD5 或 SHA1,这些哈希算法太快了适合 GPU 暴力破解。应该用专门为密码设计的慢速哈希算法:bcrypt(推荐 cost>=10)或 Argon2。password_hash 自动选择最安全的算法并生成随机盐,password_verify 验证密码。
php
<?php

// 注册时加密密码
$password = $_POST["password"];
$hash = password_hash($password, PASSWORD_BCRYPT, ["cost" => 12]);
// 存入数据库: $hash

// 登录时验证
$inputPassword = $_POST["password"];
$storedHash = $userFromDb["password"];

if (password_verify($inputPassword, $storedHash)) {
    echo "登录成功";
} else {
    echo "密码错误";
}

// 如果使用了更旧的 bcrypt cost,检查是否需要重新哈希
if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, ["cost" => 12])) {
    $newHash = password_hash($inputPassword, PASSWORD_BCRYPT, ["cost" => 12]);
    // 更新数据库
}
绝对不要自己设计密码存储方案。password_hash 已经是目前最安全的做法。

05. 文件上传安全

文件上传是另一个攻击入口,攻击者可以上传 PHP 文件来执行服务器端代码。防护措施:验证文件 MIME 类型不要只看 $_FILES['type'](可以伪造),用 finfo 检测真正的文件类型;限制文件大小;用随机文件名存储;存在 Web 目录之外加个代理脚本读。
php
<?php

$allowed = ["image/jpeg", "image/png", "application/pdf"];
$maxSize = 5 * 1024 * 1024; // 5MB

if ($_FILES["file"]["error"] === UPLOAD_ERR_OK) {
    // 检测真正的 MIME 类型
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($_FILES["file"]["tmp_name"]);
    
    if (!in_array($mime, $allowed)) {
        die("不允许的文件类型");
    }
    
    if ($_FILES["file"]["size"] > $maxSize) {
        die("文件太大了");
    }
    
    // 随机文件名 + 保留扩展名
    $ext = pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION);
    $newName = bin2hex(random_bytes(16)) . "." . $ext;
    
    // 存到 web 目录外
    $uploadDir = "/var/uploads/";
    move_uploaded_file($_FILES["file"]["tmp_name"], $uploadDir . $newName);
}
不要用 $_FILES['file']['type'] 判断文件类型,这个是客户端发的可以伪造。

知识测验

1/4正确 0

防止 SQL 注入最可靠的方法?