浅谈PHP安全规范
字数 1187 2025-08-18 11:37:45
PHP安全规范与最佳实践
1. PHP基础安全配置
1.1 敏感配置项
; 不在请求头中泄露PHP信息
expose_php=Off
; 错误处理配置
display_errors=Off
display_startup_errors=off
log_errors=On
error_log=/var/log/httpd/php_scripts_error.log
; 文件上传配置
file_uploads=On
upload_max_filesize=1M
post_max_size=1M ; 必须大于upload_max_filesize
; 远程代码执行防护
allow_url_fopen=Off
allow_url_include=Off
; 资源限制
max_execution_time = 30
max_input_time = 30
memory_limit = 40M
; 会话配置
session.save_path="/var/lib/php/session"
1.2 禁用危险函数
disable_functions = phpinfo,eval,passthru,assert,exec,system,ini_set,ini_get,get_included_files,get_defined_functions,get_defined_constants,get_defined_vars,glob,``,chroot,scandir,chgrp,chown,shell_exec,proc_open,proc_get_status,ini_alter,ini_restore,dl,pfsockopen,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server,fsocket,fsockopen
1.3 文件系统访问限制
open_basedir='/var/www/html/'
2. PHP弱类型安全问题
2.1 常见弱类型比较问题
0=='0' //true
0 == 'abcdefg' //true
1 == '1abcdef' //true
null==false //true
123=='123' //true
// 哈希比较
"0e132456789"=="0e7124511451155" //true
"0x1e240"=="123456" //true
"0x1e240"==123456 //true
// 类型转换
var_dump(intval('2')) //2
var_dump(intval('3abcd')) //3
var_dump(intval('abcd')) //0
// 数组比较
var_dump(md5($array1)==var_dump($array2)); //true
// switch比较
$i ="2abc";
switch ($i) {
case 0:case 1:case 2:
echo "i is less than 3 but not negative";
break;
case 3: echo "i is 3";
}
// in_array问题
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true
3. 常见漏洞防护
3.1 SQL注入防护
不安全示例(Low level):
$id = $_REQUEST['id'];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query($query);
安全示例(Impossible level):
// 检查Anti-CSRF token
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 获取并验证输入
$id = $_GET['id'];
if(is_numeric($id)) {
// 使用预处理语句
$data = $db->prepare('SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;');
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
if($data->rowCount() == 1) {
$row = $data->fetch();
// 安全输出
echo "<pre>ID: {$id}<br />First name: {$row['first_name']}<br />Surname: {$row['last_name']}</pre>";
}
}
3.2 CSRF防护
不安全示例(Low level):
if(isset($_GET['Change'])) {
$pass_new = $_GET['password_new'];
$pass_conf = $_GET['password_conf'];
if($pass_new == $pass_conf) {
// 直接修改密码
$insert = "UPDATE `users` SET password = '".md5($pass_new)."' WHERE user = '".dvwaCurrentUser()."'";
mysql_query($insert);
}
}
安全示例(Impossible level):
if(isset($_POST['Change'])) {
// 检查Anti-CSRF token
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 获取输入
$pass_curr = $_GET['password_current'];
$pass_new = $_GET['password_new'];
$pass_conf = $_GET['password_conf'];
// 清理和验证当前密码
$pass_curr = stripslashes($pass_curr);
$pass_curr = mysql_real_escape_string($pass_curr);
$pass_curr = md5($pass_curr);
// 验证当前密码是否正确
$data = $db->prepare('SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;');
$data->bindParam(':user', dvwaCurrentUser(), PDO::PARAM_STR);
$data->bindParam(':password', $pass_curr, PDO::PARAM_STR);
$data->execute();
// 验证新密码和确认密码是否匹配
if(($pass_new == $pass_conf) && ($data->rowCount() == 1)) {
// 更新密码
$pass_new = stripslashes($pass_new);
$pass_new = mysql_real_escape_string($pass_new);
$pass_new = md5($pass_new);
$data = $db->prepare('UPDATE users SET password = (:password) WHERE user = (:user);');
$data->bindParam(':password', $pass_new, PDO::PARAM_STR);
$data->bindParam(':user', dvwaCurrentUser(), PDO::PARAM_STR);
$data->execute();
}
}
3.3 命令注入防护
不安全示例(Low level):
$target = $_REQUEST['ip'];
if(stristr(php_uname('s'), 'Windows NT')) {
$cmd = shell_exec('ping '.$target);
} else {
$cmd = shell_exec('ping -c 4 '.$target);
}
echo "<pre>{$cmd}</pre>";
安全示例(Impossible level):
// 检查Anti-CSRF token
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 获取并验证IP地址
$target = $_REQUEST['ip'];
$target = stripslashes($target);
$octet = explode(".", $target);
if((is_numeric($octet[0])) && (is_numeric($octet[1])) &&
(is_numeric($octet[2])) && (is_numeric($octet[3])) &&
(sizeof($octet) == 4)) {
// 重建合法IP
$target = $octet[0].'.'.$octet[1].'.'.$octet[2].'.'.$octet[3];
// 执行ping命令
if(stristr(php_uname('s'), 'Windows NT')) {
$cmd = shell_exec('ping '.$target);
} else {
$cmd = shell_exec('ping -c 4 '.$target);
}
echo "<pre>{$cmd}</pre>";
} else {
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
3.4 暴力破解防护
不安全示例(Low level):
$user = $_GET['username'];
$pass = md5($_GET['password']);
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysql_query($query);
if($result && mysql_num_rows($result) == 1) {
// 登录成功
}
安全示例(Impossible level):
// 默认值
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;
// 检查数据库中的失败登录次数
$data = $db->prepare('SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
$row = $data->fetch();
// 检查用户是否被锁定
if(($data->rowCount() == 1) && ($row['failed_login'] >= $total_failed_login)) {
$last_login = strtotime($row['last_login']);
$timeout = strtotime("{$last_login} +{$lockout_time} minutes");
$timenow = strtotime("now");
if($timenow < $timeout) {
$account_locked = true;
}
}
// 验证用户名和密码
$data = $db->prepare('SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->bindParam(':password', $pass, PDO::PARAM_STR);
$data->execute();
$row = $data->fetch();
if(($data->rowCount() == 1) && ($account_locked == false)) {
// 登录成功,重置失败计数
$data = $db->prepare('UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
} else {
// 登录失败,增加失败计数
sleep(rand(2, 4)); // 随机延迟
$data = $db->prepare('UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
// 更新最后登录时间
$data = $db->prepare('UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
}
3.5 文件包含漏洞防护
不安全示例(Low level):
$file = $_GET['page'];
include($file);
安全示例(Impossible level):
$file = $_GET['page'];
// 只允许包含特定文件
if($file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php") {
echo "ERROR: File not found!";
exit;
}
include($file);
3.6 文件上传漏洞防护
不安全示例(Low level):
$target_path = "uploads/";
$target_path .= basename($_FILES['uploaded']['name']);
move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path);
安全示例(Impossible level):
// 检查Anti-CSRF token
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 文件信息
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1);
$uploaded_size = $_FILES['uploaded']['size'];
$uploaded_type = $_FILES['uploaded']['type'];
$uploaded_tmp = $_FILES['uploaded']['tmp_name'];
// 目标路径
$target_path = 'uploads/';
$target_file = md5(uniqid() . $uploaded_name) . '.' . $uploaded_ext;
// 验证文件类型和大小
if((strtolower($uploaded_ext) == 'jpg' || strtolower($uploaded_ext) == 'jpeg' || strtolower($uploaded_ext) == 'png') &&
($uploaded_size < 100000) &&
($uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png') &&
getimagesize($uploaded_tmp)) {
// 重新编码图像以去除元数据
if($uploaded_type == 'image/jpeg') {
$img = imagecreatefromjpeg($uploaded_tmp);
imagejpeg($img, $temp_file, 100);
} else {
$img = imagecreatefrompng($uploaded_tmp);
imagepng($img, $temp_file, 9);
}
imagedestroy($img);
// 移动文件
if(rename($temp_file, $target_path . $target_file)) {
echo "<pre><a href='{$target_path}{$target_file}'>{$target_file}</a> succesfully uploaded!</pre>";
}
}
3.7 XSS防护
反射型XSS不安全示例(Low level):
echo '<pre>Hello ' . $_GET['name'] . '</pre>';
存储型XSS安全示例(Impossible level):
// 检查Anti-CSRF token
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 获取并清理输入
$message = trim($_POST['mtxMessage']);
$name = trim($_POST['txtName']);
// 清理消息输入
$message = stripslashes($message);
$message = mysql_real_escape_string($message);
$message = htmlspecialchars($message);
// 清理名称输入
$name = stripslashes($name);
$name = mysql_real_escape_string($name);
$name = htmlspecialchars($name);
// 使用预处理语句插入数据库
$data = $db->prepare('INSERT INTO guestbook (comment, name) VALUES (:message, :name);');
$data->bindParam(':message', $message, PDO::PARAM_STR);
$data->bindParam(':name', $name, PDO::PARAM_STR);
$data->execute();
4. PHP伪协议
PHP支持多种伪协议,可能被用于攻击:
file:///var/www/html- 访问本地文件系统ftp://<login>:<password>@<ftpserveraddress>- 访问FTP(s) URLsdata://- 数据流http://- 访问HTTP(s) URLsphp://- 访问各个输入/输出流zlib://- 压缩流data://- Data (RFC 2397)glob://- 查找匹配的文件路径模式phar://- PHP Archivessh2://- Secure Shell 2rar://- RARogg://- Audio streamsexpect://- 处理交互式的流
5. 安全开发建议
- 永远不要信任用户输入:所有用户输入都应被视为不可信的,必须进行验证和过滤
- 使用预处理语句:防止SQL注入的最有效方法
- 实施CSRF防护:为所有敏感操作使用Anti-CSRF token
- 安全的会话管理:使用安全的会话配置和适当的会话超时
- 安全的文件处理:限制文件系统访问,安全处理文件上传
- 安全的错误处理:生产环境中不应显示详细错误信息
- 使用最新版本的PHP:保持PHP版本更新以获取安全修复
- 禁用危险函数:通过php.ini禁用不必要的危险函数
- 实施适当的访问控制:确保用户只能访问他们被授权的资源
- 安全的密码存储:使用强哈希算法如bcrypt存储密码
通过遵循这些安全规范和最佳实践,可以显著提高PHP应用程序的安全性,减少常见漏洞的风险。