一、SQL注入预处理的底层原理

很多开发者觉得用了框架就安全了,但真正理解预处理语句机制的人其实不多。咱们用最直白的例子来说:当用户输入' OR '1'='1这种经典攻击字符串时,普通查询拼接会直接导致语句变形,而预处理就像给参数穿了防弹衣。

技术栈:PHP + MySQLi

// 危险的传统拼接方式
$username = $_POST['username'];
$sql = "SELECT * FROM users WHERE username = '$username'"; 
// 当$username是"admin' -- "时,SQL就变成了SELECT * FROM users WHERE username = 'admin' -- '

// 使用预处理的安全姿势
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username); // "s"表示字符串类型
$stmt->execute();

这里的问号?是占位符,MySQL驱动会先编译SQL结构,再把用户输入当作纯数据处理。哪怕用户输入包含引号或分号,也只会被当作普通字符。

关联技术:PDO的命名参数

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->bindValue(':email', $_POST['email'], PDO::PARAM_STR);

PDO支持:name形式的参数绑定,对复杂查询更友好。注意要明确指定参数类型(如PDO::PARAM_STR),避免隐式转换风险。

二、XSS过滤的算法实战

跨站脚本攻击就像在网页里埋地雷,用户一点就炸。过滤不是简单替换<script>,而是要区分上下文——文本、HTML属性、URL、CSS都需要不同处理。

技术栈:PHP的HTMLPurifier库

require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);

$dirty_html = '<script>alert(1)</script><p>正常内容</p>';
$clean_html = $purifier->purify($dirty_html);
// 输出:<p>正常内容</p> —— 脚本被自动剥离

这个库通过白名单机制工作,只允许安全的HTML标签和属性(比如<a>href,但会检查是否以javascript:开头)。

手动过滤的陷阱

// 错误示范:只过滤<script>标签
$filtered = str_replace('<script>', '', $_POST['content']);
// 攻击者可以用<scr<script>ipt>绕过,或者通过<img onerror=alert(1)>触发

更安全的做法是输出时转义,而不是存储时过滤。例如用htmlspecialchars()处理纯文本输出:

echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
// 将< > & " '转为实体,ENT_QUOTES表示单双引号都处理

三、密码存储的加盐策略

看到还有项目用MD5存密码我就头疼。黑客早就准备好彩虹表等着撞库了。正确的姿势是:慢哈希+随机盐值+高成本系数。

技术栈:PHP的password_hash()

$password = 'user123'; // 用户原始密码
$options = [
    'cost' => 12, // 计算成本(越高越安全但越耗CPU)
    'salt' => random_bytes(16) // PHP7+会自动生成,这里仅为演示
];
$hashed = password_hash($password, PASSWORD_BCRYPT, $options);
// 输出类似:$2y$12$3K4hnAeGp2qrCz5Vb1JX.OU6dYbW1lT9GJWz7DhnmRlN9JZ4vYb1K

// 验证密码
if (password_verify($password, $hashed)) {
    echo "密码正确";
}

PASSWORD_BCRYPT使用Blowfish算法,自动包含盐值在哈希结果中。cost建议设为10-12,服务器能承受的最高值。

常见误区

  1. 全局固定盐值:相当于没加盐,黑客可以针对这个盐值预计算哈希表
  2. 多次哈希:md5(sha1(password))反而可能降低安全性
  3. 自己实现加密:99%的情况下,你的算法没有专业密码学家设计的可靠

四、综合防御体系搭建

安全不是单点防护,而是立体防御。比如一个登录功能要同时考虑:

  1. SQL注入:预处理语句
  2. XSS:输出转义 + CSP头
  3. CSRF:表单令牌
  4. 暴力破解:验证码 + 登录限速
  5. 密码泄露:哈希加盐 + HTTPS传输

技术栈:PHP实战示例

// 登录接口安全增强版
session_start();

// 1. CSRF令牌校验
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('非法请求');
}

// 2. 登录限速(Redis记录失败次数)
$redis = new Redis();
$redis->connect('127.0.0.1');
$key = 'login_attempts:' . $_SERVER['REMOTE_ADDR'];
if ($redis->get($key) > 5) {
    die('尝试次数过多,请稍后再试');
}

// 3. 参数预处理
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();

// 4. 密码验证
if ($user && password_verify($_POST['password'], $user['password'])) {
    $_SESSION['user_id'] = $user['id'];
    $redis->del($key); // 清除失败计数
} else {
    $redis->incr($key);
    $redis->expire($key, 3600); // 1小时内有效
    die('用户名或密码错误');
}

五、安全开发的思维转变

很多漏洞源于开发者"信任用户输入"的思维。现代安全需要:

  1. 默认不信任原则:所有输入都是恶意的,直到被证明安全
  2. 最小权限原则:数据库用户只给必要权限(比如只读权限的账号)
  3. 纵深防御:即使一层防护失效,其他层还能拦截
  4. 及时更新:PHP本身就有很多安全更新,比如5.6之前版本的哈希缺陷

记住:安全不是功能,而是质量属性。就像建筑里的消防通道,平时用不到,但关键时刻能救命。