Web安全是每个PHP程序员必须重视的问题。SQL注入和XSS攻击是最常见、最危险的两种Web安全漏洞,如果不做好防护,网站很容易被黑客攻击,数据泄露,甚至服务器被接管。今天就来分享PHP安全编程的实战经验,重点讲解如何防止SQL注入和XSS攻击。

1. SQL注入防护

SQL注入是指攻击者通过在输入中插入恶意SQL代码,从而操纵数据库查询的攻击方式。SQL注入可能导致数据泄露、数据篡改、数据删除,甚至服务器被接管。

不安全的写法:

// 危险!直接拼接用户输入到SQL中
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysqli_query($conn, $sql);

攻击者可以传入 id=1 OR 1=1,这样SQL就变成了 SELECT * FROM users WHERE id = 1 OR 1=1,会返回所有用户数据。更危险的是,攻击者还可以传入 id=1; DROP TABLE users;,直接删除表。

安全的写法:使用预处理语句(Prepared Statements)

预处理语句是防止SQL注入最有效的方法。预处理语句会先发送SQL模板给数据库,数据库编译后,再发送参数,参数会被当作纯数据处理,不会被当作SQL代码执行。

PDO预处理:

// PDO预处理
$id = $_GET['id'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

MySQLi预处理:

// MySQLi预处理
$id = $_GET['id'];
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();

LIKE查询的预处理:

// LIKE查询需要自己加通配符
$keyword = $_GET['keyword'];
$stmt = $pdo->prepare("SELECT * FROM posts WHERE title LIKE :keyword");
$stmt->execute(['keyword' => "%$keyword%"]);

IN查询的预处理:

// IN查询需要动态生成占位符
$ids = $_GET['ids']; // 数组
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

其他SQL注入防护措施:

  1. 永远不要信任用户输入,所有用户输入都要验证和过滤
  2. 使用最小权限原则,数据库用户只给必要的权限
  3. 关闭错误显示,生产环境不要显示数据库错误信息
  4. 使用WAF(Web应用防火墙)作为额外防护
  5. 定期安全审计和渗透测试

2. XSS攻击防护

XSS(跨站脚本攻击)是指攻击者在网页中注入恶意脚本,当其他用户访问该网页时,恶意脚本会在用户浏览器中执行,从而窃取用户信息、劫持用户会话、篡改网页内容等。

XSS分为三种类型:

  • 存储型XSS:恶意脚本存储在服务器数据库中,用户访问时执行
  • 反射型XSS:恶意脚本通过URL参数传递,服务器反射回页面执行
  • DOM型XSS:恶意脚本通过修改DOM执行,不经过服务器

不安全的写法:

// 危险!直接输出用户输入,不做转义
echo $_GET['name'];
echo $user['bio'];

攻击者可以传入 name=<script>alert('XSS')</script>,这样恶意脚本就会在用户浏览器中执行。

安全的写法:使用htmlspecialchars转义输出

htmlspecialchars 会把特殊字符转换为HTML实体,从而防止XSS攻击。

// 安全!转义输出
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
echo htmlspecialchars($user['bio'], ENT_QUOTES, 'UTF-8');

htmlspecialchars 会转换以下字符:

  • &&amp;
  • "&quot;
  • '&#039;(当ENT_QUOTES时)
  • <&lt;
  • >&gt;

封装转义函数:

// 封装转义函数,方便使用
function e($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

// 使用
echo e($_GET['name']);
echo e($user['bio']);

不同上下文的XSS防护:

XSS防护要根据输出的上下文来选择合适的转义方式:

  1. HTML内容中:使用htmlspecialchars
<div><?php echo e($content); ?></div>
  1. HTML属性中:使用htmlspecialchars,并且属性值用引号括起来
<input type="text" value="<?php echo e($value); ?>">
  1. JavaScript中:使用json_encode
<script>
var data = <?php echo json_encode($data); ?>;
</script>
  1. URL中:使用urlencode
<a href="search?q=<?php echo urlencode($keyword); ?>">搜索</a>
  1. CSS中:尽量不要在CSS中使用用户输入,如果必须使用,要严格验证

其他XSS防护措施:

  1. Content Security Policy (CSP):设置CSP头,限制脚本来源,防止XSS
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
  1. HttpOnly Cookie:设置Cookie的HttpOnly属性,防止JavaScript读取Cookie
setcookie("session", $sessionId, time()+3600, "/", "", true, true);
  1. 输入验证:对用户输入进行验证,只允许合法的字符和格式
  2. 使用成熟的模板引擎:如Twig、Blade等,模板引擎默认会自动转义输出
  3. 定期更新依赖:及时更新框架和库,修复已知的安全漏洞

3. 其他安全注意事项

1. CSRF防护:使用CSRF Token防止跨站请求伪造

session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// 表单中添加Token
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';

// 验证Token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die("CSRF token validation failed");
}

2. 文件上传安全

  • 验证文件类型和扩展名
  • 限制文件大小
  • 重命名上传文件
  • 将上传文件存储在Web根目录之外
  • 不要执行上传目录中的PHP文件

3. 密码安全

  • 使用password_hash哈希密码,不要用MD5、SHA1
  • 使用password_verify验证密码
  • 强制用户使用强密码
  • 敏感操作需要重新验证密码
// 哈希密码
$hash = password_hash($password, PASSWORD_DEFAULT);

// 验证密码
if (password_verify($password, $hash)) {
    // 密码正确
}

4. 会话安全

  • 使用sessionregenerateid防止会话固定
  • 设置会话超时
  • 敏感操作使用HTTPS
  • 销毁会话时彻底清除会话数据

总结

Web安全是每个PHP程序员必须重视的问题。SQL注入和XSS攻击是最常见、最危险的两种Web安全漏洞,必须做好防护。

防止SQL注入的核心是使用预处理语句,永远不要直接拼接用户输入到SQL中。防止XSS攻击的核心是转义输出,根据不同的上下文选择合适的转义方式。

安全没有一劳永逸,需要持续关注和维护。希望这篇文章能帮助大家写出更安全的PHP代码。