Web安全是每个PHP开发者必须掌握的技能。根据OWASP(开放式Web应用程序安全项目)的统计,注入攻击(包括SQL注入)、XSS跨站脚本攻击、CSRF跨站请求伪造,一直是Web应用最常见、危害最大的安全漏洞。作为PHP开发者,我们必须了解这些攻击的原理,并掌握相应的防御方法。
2016年,PHP 7已经发布,PDO扩展已经成为数据库操作的标准方式,但是很多开发者依然在使用老旧的mysql_*函数,或者在使用PDO时没有正确使用预处理语句,导致SQL注入漏洞依然普遍存在。同时,XSS和CSRF攻击也因为开发者安全意识不足而频繁发生。
今天就来详细讲解PHP安全编程,重点讲解SQL注入、XSS、CSRF三大常见Web安全威胁的原理和防御方法。
一、SQL注入(SQL Injection)
1. 什么是SQL注入
SQL注入是指攻击者通过在Web应用的输入框中注入恶意的SQL代码,从而操纵数据库,获取、修改或删除数据,甚至获取服务器权限的攻击方式。
举个简单的例子,假设我们有一个登录功能,代码如下:
// 危险代码!存在SQL注入漏洞
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysql_query($sql);如果攻击者在用户名输入框中输入 admin' --,那么SQL语句就变成了:
SELECT * FROM users WHERE username='admin' --' AND password='xxx'-- 是SQL的注释符号,后面的内容被注释掉了,所以攻击者不需要密码就能以admin身份登录。这就是最经典的SQL注入攻击。
更严重的情况下,攻击者可以通过 UNION SELECT 获取其他表的数据,通过 INSERT/UPDATE/DELETE 修改或删除数据,甚至通过 LOAD_FILE、INTO OUTFILE 读取服务器文件或写入webshell,获取服务器权限。
2. SQL注入的防御方法
方法一:使用PDO预处理语句(推荐)
PDO(PHP Data Objects)的预处理语句(Prepared Statement)是防御SQL注入的最佳方式。预处理语句将SQL语句和参数分开,参数会被自动转义,从根本上防止SQL注入。
// 安全代码:使用PDO预处理语句
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$username = $_POST['username'];
$password = $_POST['password'];
// 使用命名占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([':username' => $username, ':password' => $password]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 或者使用问号占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);注意:PDO预处理语句默认是模拟预处理(EMULATE_PREPARES=true),在某些情况下可能存在安全风险。建议关闭模拟预处理,使用真正的预处理:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);方法二:使用mysqli预处理语句
如果使用mysqli扩展,也可以使用预处理语句:
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param('ss', $username, $password);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();方法三:对输入进行转义和验证(不推荐作为唯一防御)
如果必须使用字符串拼接SQL(不推荐),可以对输入进行转义:
// 不推荐,转义不如预处理安全
$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password'";但是,转义不是100%安全的,特别是在字符集设置不正确的情况下,可能存在宽字节注入等绕过方式。因此,预处理语句是首选。
方法四:限制数据库用户权限
Web应用连接数据库的用户,不应该授予过高的权限。只授予必要的权限(如SELECT、INSERT、UPDATE),不要授予DROP、ALTER、FILE等危险权限。这样即使发生SQL注入,攻击者的破坏范围也会受到限制。
3. SQL注入防御总结
- ✅ 使用PDO或mysqli的预处理语句(首选)
- ✅ 关闭PDO模拟预处理(ATTREMULATEPREPARES=false)
- ✅ 设置正确的字符集(utf8mb4)
- ✅ 限制数据库用户权限
- ❌ 不要使用已废弃的mysql_*函数
- ❌ 不要直接拼接用户输入到SQL语句中
- ❌ 不要只依赖输入转义
二、XSS跨站脚本攻击(Cross-Site Scripting)
1. 什么是XSS
XSS(Cross-Site Scripting,跨站脚本攻击)是指攻击者在Web页面中注入恶意的JavaScript代码,当其他用户访问该页面时,恶意代码会在用户的浏览器中执行,从而窃取用户信息(如Cookie、Session)、篡改页面内容、诱导用户操作,甚至控制用户浏览器。
XSS分为三种类型:
反射型XSS(Reflected XSS):恶意脚本通过URL参数传递,服务器将参数反射到页面中。例如:
// 危险代码!存在反射型XSS漏洞
echo "搜索结果:" . $_GET['keyword'];攻击者构造URL:http://example.com/search?keyword=<script>alert(document.cookie)</script>,用户点击后,恶意脚本会在页面中执行。
存储型XSS(Stored XSS):恶意脚本被存储在服务器(如数据库)中,当其他用户访问包含该数据的页面时,恶意脚本被执行。例如评论区、留言板、用户资料等,如果用户输入的内容没有经过过滤就存储和显示,就会存在存储型XSS漏洞。存储型XSS比反射型危害更大,因为它会影响所有访问该页面的用户。
DOM型XSS(DOM-based XSS):恶意脚本通过修改页面DOM来执行,不经过服务器。例如:
// 危险代码!存在DOM型XSS漏洞
document.getElementById('output').innerHTML = location.hash;攻击者构造URL:http://example.com/#<img src=x onerror=alert(1)>,JavaScript读取hash值并写入innerHTML,导致恶意脚本执行。
2. XSS的防御方法
方法一:对输出进行HTML转义(最基本、最重要)
在将用户输入输出到HTML页面时,必须使用 htmlspecialchars() 函数进行转义,将特殊字符(<、>、"、'、&)转换为HTML实体。
// 安全代码:对输出进行HTML转义
echo htmlspecialchars($_GET['keyword'], ENT_QUOTES, 'UTF-8');
// 封装一个函数方便使用
function e($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
echo e($user['name']);htmlspecialchars() 的参数说明:
ENT_QUOTES:同时转义双引号和单引号ENT_HTML5:HTML5文档类型(PHP 5.4+)- 字符集:指定为UTF-8,避免编码问题
方法二:使用Content Security Policy(CSP)
CSP(内容安全策略)是一种HTTP响应头,通过限制页面可以加载和执行的资源来源,来防御XSS攻击。即使攻击者成功注入了脚本,CSP也能阻止脚本执行。
// 设置CSP响应头
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;");CSP指令说明:
default-src:默认策略,所有资源类型都遵循script-src:允许加载的JavaScript来源style-src:允许加载的CSS来源img-src:允许加载的图片来源'self':只允许同源资源'unsafe-inline':允许内联脚本和样式(不推荐,降低安全性)'nonce-xxx':只允许带有特定nonce值的内联脚本(推荐)
最严格的CSP可以完全禁止内联脚本,只允许加载指定来源的外部脚本,这样即使有XSS注入,脚本也无法执行。
方法三:HttpOnly Cookie
将Cookie设置为HttpOnly,可以防止JavaScript通过 document.cookie 读取Cookie,即使发生XSS,攻击者也无法窃取用户的Cookie。
// 设置HttpOnly Cookie
setcookie('session', $sessionId, time() + 3600, '/', '', true, true);
// 最后一个参数true表示HttpOnly在php.ini中也可以全局设置:
session.cookie_httponly = On方法四:输入验证和过滤
对用户输入进行验证和过滤,只允许符合预期格式的输入。例如:
- 邮箱地址:使用
filtervar($email, FILTERVALIDATE_EMAIL)验证 - URL:使用
filtervar($url, FILTERVALIDATE_URL)验证 - 整数:使用
filtervar($id, FILTERVALIDATE_INT)验证 - 正则表达式:使用
preg_match()验证输入格式
但是,输入验证不能替代输出转义,因为攻击者可能绕过验证,而且不同的输出上下文(HTML、JavaScript、CSS、URL)需要不同的转义方式。
方法五:针对不同上下文使用不同的转义方式
XSS的防御不能只依赖 htmlspecialchars(),因为在不同的输出上下文中,需要不同的转义方式:
// HTML内容上下文
echo htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
// HTML属性上下文
echo htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
// JavaScript上下文
echo json_encode($data); // 使用json_encode转义
// CSS上下文
echo htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
// URL参数上下文
echo urlencode($data); // 使用urlencode3. XSS防御总结
- ✅ 对所有用户输入的输出使用
htmlspecialchars()转义 - ✅ 设置Content Security Policy(CSP)响应头
- ✅ 设置HttpOnly Cookie
- ✅ 对用户输入进行验证和过滤
- ✅ 针对不同输出上下文使用不同转义方式
- ❌ 不要直接输出用户输入到HTML页面
- ❌ 不要只依赖输入验证(验证不能替代转义)
- ❌ 不要使用
strip_tags()作为唯一防御(可能被绕过)
三、CSRF跨站请求伪造(Cross-Site Request Forgery)
1. 什么是CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造)是指攻击者诱导已登录的用户访问恶意网站,恶意网站自动向目标网站发送请求(利用用户已登录的身份),从而执行非用户本意的操作(如转账、修改密码、删除数据等)。
举个例子,假设银行网站的转账接口是:
http://bank.com/transfer?to=attacker&amount=1000用户登录了银行网站,Cookie中保存了登录状态。然后用户访问了攻击者的恶意网站,恶意网站中包含:
<img src="http://bank.com/transfer?to=attacker&amount=1000" width="0" height="0">当用户访问恶意网站时,浏览器会自动加载这个img标签,向银行网站发送转账请求。由于用户已经登录了银行网站,浏览器会自动带上银行网站的Cookie,银行网站会认为这是用户本人的操作,从而执行转账。这就是CSRF攻击。
CSRF攻击的特点:
- 攻击者利用用户已登录的身份
- 请求是从第三方网站发起的
- 用户可能完全不知情
- 攻击的是状态改变的操作(如转账、修改、删除),而不是获取数据
2. CSRF的防御方法
方法一:使用CSRF Token(推荐)
CSRF Token是防御CSRF最常用、最有效的方法。原理是:在表单中嵌入一个随机生成的Token,提交表单时验证Token是否正确。由于攻击者无法获取到用户的Token(Token在用户的页面中,受同源策略保护),所以无法伪造请求。
实现步骤:
// 1. 生成CSRF Token并存储在Session中
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['csrf_token'];
// 2. 在表单中嵌入Token
echo '<form method="POST" action="transfer.php">';
echo '<input type="hidden" name="csrf_token" value="' . $csrfToken . '">';
echo '<input type="text" name="to">';
echo '<input type="text" name="amount">';
echo '<button type="submit">转账</button>';
echo '</form>';
// 3. 提交时验证Token
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// Token验证失败,拒绝请求
http_response_code(403);
die('CSRF token validation failed');
}
// Token验证通过,处理业务逻辑
// ...
}对于AJAX请求,可以将Token放在请求头中:
// 在AJAX请求头中携带CSRF Token
var xhr = new XMLHttpRequest();
xhr.open('POST', 'transfer.php', true);
xhr.setRequestHeader('X-CSRF-Token', '<?php echo $csrfToken; ?>');
xhr.send(data);PHP端验证:
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if ($token !== $_SESSION['csrf_token']) {
http_response_code(403);
die('CSRF token validation failed');
}方法二:验证Referer头
HTTP请求头中的Referer字段表示请求的来源页面。通过验证Referer是否来自本站,可以防御CSRF攻击。
// 验证Referer
if (isset($_SERVER['HTTP_REFERER'])) {
$refererHost = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
if ($refererHost !== 'example.com') {
http_response_code(403);
die('Invalid referer');
}
} else {
// 没有Referer头,可能被攻击,拒绝请求
http_response_code(403);
die('Missing referer');
}但是,Referer验证不是100%可靠的:
- 某些浏览器或隐私工具会不发送Referer头
- Referer可能被伪造(虽然比较困难)
- HTTPS到HTTP的跳转不发送Referer
因此,Referer验证可以作为辅助手段,但不应该作为唯一防御。
方法三:SameSite Cookie属性
SameSite是Cookie的一个属性,可以限制Cookie在跨站请求中的发送,从而防御CSRF攻击。
// 设置SameSite Cookie
setcookie('session', $sessionId, [
'expires' => time() + 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict' // 或 'Lax'
]);SameSite属性值:
Strict:最严格,任何跨站请求都不发送CookieLax:宽松模式,跨站的GET请求会发送Cookie(如链接跳转),但POST请求不发送(大多数CSRF攻击是POST)None:不限制,需要同时设置Secure属性
SameSite Cookie是防御CSRF的有效手段,但需要浏览器支持(现代浏览器都支持)。建议同时使用CSRF Token和SameSite Cookie,双重防御。
方法四:关键操作要求重新验证身份
对于敏感操作(如转账、修改密码、删除账户),要求用户重新输入密码或进行二次验证(如短信验证码、邮箱验证码),即使发生CSRF,攻击者也无法完成这些操作。
3. CSRF防御总结
- ✅ 使用CSRF Token(首选,最有效)
- ✅ 设置SameSite Cookie属性
- ✅ 验证Referer头(辅助手段)
- ✅ 关键操作要求重新验证身份
- ❌ 不要只依赖Cookie进行身份验证
- ❌ 不要使用GET请求执行状态改变的操作(GET应该是幂等的)
- ❌ 不要在URL参数中传递敏感信息
四、其他PHP安全注意事项
1. 密码安全
- 使用
passwordhash()和passwordverify()处理密码,不要自己写加密算法 - 使用
passwordhash($password, PASSWORDDEFAULT),自动选择最佳算法 - 不要使用MD5、SHA1等不安全的哈希算法
- 不要明文存储密码
// 注册时哈希密码
$passwordHash = password_hash($_POST['password'], PASSWORD_DEFAULT);
// 存储$passwordHash到数据库
// 登录时验证密码
if (password_verify($_POST['password'], $user['password_hash'])) {
// 密码正确
}2. 文件上传安全
- 验证文件类型(不要只依赖扩展名,要验证MIME类型和文件内容)
- 限制文件大小
- 将上传文件存储在Web根目录之外,或重命名文件
- 不要将上传目录设置为可执行
- 使用
isuploadedfile()和moveuploadedfile()处理上传
3. 会话安全
- 使用
sessionregenerateid()在登录后重新生成Session ID,防止会话固定 - 设置Session Cookie为HttpOnly和Secure
- 设置合理的Session过期时间
- 敏感操作后销毁Session
4. 错误信息安全
- 生产环境关闭错误显示(
display_errors = Off) - 将错误记录到日志文件(
log_errors = On) - 不要向用户暴露详细的错误信息(如数据库错误、文件路径等)
- 使用自定义错误页面
5. HTTPS
- 全站使用HTTPS,加密数据传输
- 设置HSTS(HTTP Strict Transport Security)响应头
- 不要在HTTP页面中混合加载HTTPS资源
总结
Web安全是每个PHP开发者必须掌握的技能。SQL注入、XSS、CSRF是最常见的三大Web安全威胁,本文详细讲解了它们的原理和防御方法:
- SQL注入:使用PDO/mysqli预处理语句,从根本上防止注入
- XSS:对输出进行HTML转义,设置CSP和HttpOnly Cookie
- CSRF:使用CSRF Token,设置SameSite Cookie,验证Referer
安全是一个持续的过程,不是一次性的工作。作为开发者,我们要时刻保持安全意识,关注最新的安全漏洞和防御方法,定期审计代码,确保Web应用的安全。
"安全无小事",希望每个PHP开发者都能重视安全,写出更安全、更可靠的代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录