一、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,服务器能承受的最高值。
常见误区
- 全局固定盐值:相当于没加盐,黑客可以针对这个盐值预计算哈希表
- 多次哈希:
md5(sha1(password))反而可能降低安全性 - 自己实现加密:99%的情况下,你的算法没有专业密码学家设计的可靠
四、综合防御体系搭建
安全不是单点防护,而是立体防御。比如一个登录功能要同时考虑:
- SQL注入:预处理语句
- XSS:输出转义 + CSP头
- CSRF:表单令牌
- 暴力破解:验证码 + 登录限速
- 密码泄露:哈希加盐 + 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('用户名或密码错误');
}
五、安全开发的思维转变
很多漏洞源于开发者"信任用户输入"的思维。现代安全需要:
- 默认不信任原则:所有输入都是恶意的,直到被证明安全
- 最小权限原则:数据库用户只给必要权限(比如只读权限的账号)
- 纵深防御:即使一层防护失效,其他层还能拦截
- 及时更新:PHP本身就有很多安全更新,比如5.6之前版本的哈希缺陷
记住:安全不是功能,而是质量属性。就像建筑里的消防通道,平时用不到,但关键时刻能救命。
评论