1. SQL注入#
Sink点#
mysql_query()mysqli_query()mysqli::query()PDO::query()PDOStatement::execute()PDO::prepare()
审计检查项#
- 是否使用参数化查询
- SQL字符串是否通过拼接构造
- 用户输入是否直接进入SQL
- 是否使用
PDO::ATTR_EMULATE_PREPARES=false - DSN中是否指定字符集
- 是否使用
bindParam()或bindValue() - 特殊子句是否有白名单验证
风险代码模式#
// 模式1:直接拼接
$username = $_GET['username'];
$query = "SELECT * FROM users WHERE username = '" . $username . "'";
$result = mysqli_query($conn, $query);php// 模式2:PDO未关闭模拟预处理
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
// PDO::ATTR_EMULATE_PREPARES默认为true
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);php// 模式3:mysql_*函数
$username = mysql_real_escape_string($_GET['username']);
// 即使使用转义,也可能被绕过php安全实现#
// 方案1:PDO参数化(完整配置)
$pdo = new PDO(
"mysql:host=localhost;dbname=test;charset=utf8mb4",
"user",
"pass",
[PDO::ATTR_EMULATE_PREPARES => false]
);
$username = $_GET['username'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$users = $stmt->fetchAll();php// 方案2:命名占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->bindParam(':username', $_GET['username'], PDO::PARAM_STR);
$stmt->execute();php// 方案3:MySQLi参数化
$username = $_GET['username'];
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();php2. 命令执行#
Sink点#
shell_exec()exec()system()passthru()popen()proc_open()eval()assert()
审计检查项#
- 用户输入是否直接作为命令
- 是否通过shell执行(
/bin/sh -c) - 是否实现了命令白名单
- php.ini的
disable_functions配置 - 是否过滤了特殊字符
- 反引号是否被使用
风险代码模式#
// 模式1:直接执行用户输入
$cmd = $_GET['cmd'];
$output = shell_exec($cmd);php// 模式2:通过shell执行
$host = $_GET['host'];
echo shell_exec("ping " . $host);php// 模式3:简单的字符替换过滤
$cmd = str_replace(";", "", $_GET['cmd']);
// 但| 仍可使用php安全实现#
// 方案1:命令白名单
$cmd = $_GET['cmd'];
$allowedCommands = ['whoami', 'date', 'pwd'];
if (!in_array($cmd, $allowedCommands)) {
die("Command not allowed");
}
echo shell_exec($cmd);php// 方案2:参数白名单
$host = $_GET['host'];
$allowedHosts = ['google.com', 'github.com', 'example.com'];
if (!in_array($host, $allowedHosts)) {
die("Host not allowed");
}
echo shell_exec("ping -c 4 " . escapeshellarg($host));php3. 文件上传和任意文件写入#
Sink点#
move_uploaded_file()fopen() + fwrite()file_put_contents()chmod()mkdir()
审计检查项#
- 扩展名是否进行白名单验证
- 是否防止了
.php、.phtml等可执行文件 - MIME类型是否经过验证
- 文件名是否包含路径分隔符
- 是否使用
basename()清理 - 保存路径是否在预期目录内
- 是否生成了新文件名
- 最终路径是否被规范化验证
风险代码模式#
// 模式1:直接使用上传文件名
if ($_FILES['file']['error'] == UPLOAD_ERR_OK) {
$filename = $_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $filename);
}php// 模式2:仅检查扩展名
if (strpos($_FILES['file']['name'], '.jpg') !== false) {
move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $_FILES['file']['name']);
}php// 模式3:使用客户端MIME
if ($_FILES['file']['type'] == 'image/jpeg') {
move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $_FILES['file']['name']);
}php安全实现#
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$max_file_size = 5 * 1024 * 1024;
if ($_FILES['file']['error'] != UPLOAD_ERR_OK) {
die("Upload error");
}
if ($_FILES['file']['size'] > $max_file_size) {
die("File too large");
}
$filename = $_FILES['file']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
die("File type not allowed");
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed_mimes)) {
die("Invalid MIME type");
}
$new_filename = bin2hex(random_bytes(16)) . '.' . $ext;
$upload_dir = '/absolute/path/to/uploads';
$upload_path = $upload_dir . DIRECTORY_SEPARATOR . $new_filename;
$upload_path = realpath(dirname($upload_path)) . DIRECTORY_SEPARATOR . $new_filename;
if (strpos($upload_path, realpath($upload_dir)) !== 0) {
die("Invalid file path");
}
if (!move_uploaded_file($_FILES['file']['tmp_name'], $upload_path)) {
die("Failed to move uploaded file");
}
chmod($upload_path, 0644);php4. 反序列化漏洞#
Sink点#
unserialize()json_decode()
审计检查项#
- 是否对不可信输入执行反序列化
- 数据来源是否可信
- 是否实现了类白名单
- 是否防止Phar伪协议利用
风险代码模式#
// 模式1:直接反序列化
$data = $_GET['data'];
$obj = unserialize($data);php// 模式2:从Cookie反序列化
$user = unserialize($_COOKIE['user_data']);php// 模式3:Phar反序列化
$file = $_GET['file'];
if (file_exists($file)) { // phar://exploit.phar触发反序列化
// 处理文件
}php安全实现#
// 方案1:使用白名单
$allowed_classes = ['User', 'Product'];
$data = $_GET['data'];
$obj = unserialize($data, ['allowed_classes' => $allowed_classes]);php// 方案2:禁止所有类反序列化
$obj = unserialize($_GET['data'], ['allowed_classes' => false]);php// 方案3:使用JSON替代
$obj = json_decode($_GET['data'], false);php5. 任意文件读取#
Sink点#
file_get_contents()readfile()fopen() + fread()include() / require()file()
审计检查项#
- 用户参数是否直接作为文件路径
- 是否使用
basename()清理 - 是否使用
realpath()规范化 - 最终路径是否在允许目录内
- 是否实现了文件白名单
安全实现#
// 方案1:文件白名单
$allowed_files = ['readme.txt', 'help.txt', 'faq.txt'];
$file = $_GET['file'];
if (!in_array($file, $allowed_files)) {
die("File not found");
}
echo file_get_contents("files/" . $file);php// 方案2:路径规范化验证
$basedir = realpath(__DIR__ . '/files');
$filepath = realpath($basedir . '/' . $_GET['file']);
if ($filepath === false || strpos($filepath, $basedir) !== 0) {
die("Access denied");
}
echo file_get_contents($filepath);php6. 路径遍历#
Sink点#
- 文件操作
审计检查项#
- 路径是否包含
..、./ - 是否使用
realpath()规范化 - 规范化后路径是否在基础目录内
安全实现#
$basedir = realpath(__DIR__ . '/downloads');
$filepath = realpath($basedir . '/' . $_GET['file']);
if ($filepath === false || strpos($filepath, $basedir) !== 0) {
die("Access denied");
}
echo file_get_contents($filepath);php7. XXE (XML External Entity)#
Sink点#
simplexml_load_string()simplexml_load_file()DOMDocument::load()DOMDocument::loadXML()
审计检查项#
- 是否对不可信XML进行解析
- 外部实体是否被禁用
- DTD处理是否被禁用
安全实现#
libxml_disable_entity_loader(true);
$xml = simplexml_load_string($_POST['xml']);php8. SSRF (Server-Side Request Forgery)#
Sink点#
file_get_contents()fopen()curl_exec()fsockopen()
审计检查项#
- 是否允许用户指定URL
- 是否检测内部地址(127.0.0.1)
- 是否限制了协议(仅HTTP/HTTPS)
- 是否防止了DNS重绑定
安全实现#
$url = $_GET['url'];
$parsed = parse_url($url);
if (!$parsed) die("Invalid URL");
if (!in_array($parsed['scheme'], ['http', 'https'])) {
die("Invalid protocol");
}
$host = $parsed['host'];
if (filter_var($host, FILTER_VALIDATE_IP)) {
$ip = ip2long($host);
if (($ip >= ip2long('10.0.0.0') && $ip <= ip2long('10.255.255.255')) ||
($ip >= ip2long('172.16.0.0') && $ip <= ip2long('172.31.255.255')) ||
($ip >= ip2long('192.168.0.0') && $ip <= ip2long('192.168.255.255'))) {
die("Private IP address not allowed");
}
}
$allowed_domains = ['example.com', 'api.example.com'];
if (!in_array($host, $allowed_domains)) {
die("Domain not in whitelist");
}
$content = file_get_contents($url);php9. 代码执行#
eval#
Sink点:eval()
检查项:是否使用了eval
assert#
Sink点:assert()
检查项:是否使用了assert
文件包含#
Sink点:include() / require()
检查项:用户输入是否用于包含
安全实现(文件包含)#
$allowed_pages = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowed_pages)) {
die("Page not found");
}
include("pages/" . $page . ".php");php10. 模板注入#
Sink点#
Twig_Loader_Stringtemplate.render()
审计检查项#
- 是否允许用户编辑模板
- 是否将用户输入作为模板
安全实现#
$loader = new Twig_Loader_Filesystem('templates/');
$twig = new Twig_Environment($loader);
$output = $twig->render('index.html', ['name' => $_GET['name']]);php11. 安全配置#
关键php.ini设置:
disable_functions = eval, assert, system, exec, shell_exec, passthru, proc_open
allow_url_include = Off
allow_url_fopen = Off
upload_max_filesize = 5M
max_file_uploads = 20
display_errors = Off
log_errors = On
session.cookie_httponly = On
session.cookie_secure = On
expose_php = Off
open_basedir = /var/www/html:/tmp:/usr/share/phpini12. 审计工具#
- 静态分析:RIPS、SonarQube、Fortify、Semgrep
- 代码规范:PHPCS、PHPStan、Psalm
- 依赖检查:Composer Audit