本文目录导读:

PHP 安全设计是一个系统工程,不能只靠“加一个函数”来解决,其核心思想是:永远不要信任用户的输入。
以下是 PHP 安全设计的核心原则,按照重要程度和实践场景分类:
核心防御原则(输入与输出)
这是 Web 安全的基础,几乎所有的漏洞(SQL注入、XSS、命令注入)都是因为没有做好这两点。
过滤输入(Filter Input)
- 含义:接收用户数据时,验证它是否符合预期格式。
- 做法:
- 类型检查:
is_numeric(),ctype_digit(),gettype()。 - 正则/规则验证:邮箱用
filter_var($email, FILTER_VALIDATE_EMAIL),URL 用FILTER_VALIDATE_URL。 - 白名单:如果参数只能是
a,b,c,则直接判断in_array($input, ['a', 'b', 'c'])。 - 长度限制:用
strlen()或mb_strlen()限制最大长度。
- 类型检查:
转义输出(Escape Output)
- 含义:数据输出到不同上下文(HTML, JavaScript, SQL, URL)时,要进行对应的转义。
- 做法:
- 输出到 HTML 体:使用
htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8')。这是防止 XSS 的最基本动作。 - 输出到 HTML 属性:同上,但要确保属性值是双引号包围的。
- 输出到 JavaScript:不要直接用 PHP 拼接 JS 字符串,使用
json_encode()将数据转为 JSON 字符串,然后在 JS 中解析。 - 输出到 URL:使用
urlencode()或rawurlencode()。 - 输出到 SQL:使用参数化查询,不要手动转义字符串。
- 输出到 HTML 体:使用
纵深防御(各个攻击面的具体防护)
SQL 注入 —— 使用 Prepared Statements
-
原则:绝对不要拼接 SQL 字符串。
-
做法:使用 PDO 或 MySQLi 的预处理语句。
// 正确做法 $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email'); $stmt->execute(['email' => $userInput]); // 数据库驱动会处理转义 // 错误做法(高危) $sql = "SELECT * FROM users WHERE email = '" . $userInput . "'";
跨站脚本攻击 —— CSP + 转义
- 原则:除了转义输出,还应设置 HTTP 头。
- Content-Security-Policy:在响应头中限制资源加载来源。
// 只允许加载同源的脚本 header("Content-Security-Policy: script-src 'self'"); - HttpOnly Cookie:防止 XSS 窃取会话。
setcookie('session_id', $sessionId, ['httponly' => true, 'samesite' => 'Strict']);
跨站请求伪造 —— 验证来源
- 原则:任何执行写操作(修改、删除、转账)的请求,都必须验证是用户主动发出的。
- 做法:
- CSRF Token:在表单中生成一个随机 Token,存储在 Session 中,提交时对比。
- SameSite Cookie:设置
SameSite=Strict或Lax。 - 验证 Referer:检查 HTTP Referer 头,但可能被篡改。
文件上传 —— 最危险的入口之一
- 原则:永远不要信任文件的扩展名和 MIME 类型。
- 做法:
- 检查真实类型:使用
finfo_file()检查文件魔数(Magic Bytes),而不仅仅是$_FILES['file']['type']。 - 重命名文件:上传后不要保留用户原来的文件名,生成随机文件名(如
md5(time() . rand()) . 'jpg')。 - 限制路径:将文件存储到 Web 根目录之外,通过 PHP 脚本读取下载(便于控制权限和防直接执行)。
- 禁用执行权限:在
nginx/apache配置中,禁止 uploads 目录执行 PHP。
- 检查真实类型:使用
文件包含漏洞(LFI/RFI)
-
原则:避免动态文件包含。
-
做法:
- 如果必须动态包含,使用白名单映射。
// 危险做法 include($_GET['page'] . '.php');
// 安全做法 $allowed_pages = ['home', 'about', 'contact']; $page = in_array($_GET['page'], $allowed_pages) ? $_GET['page'] : 'home'; include($page . '.php');
- 禁用 `allow_url_include`(默认已禁,但需检查)。 - 如果必须动态包含,使用白名单映射。
配置与运行环境安全
禁用危险函数
- 在
php.ini的disable_functions中禁用以下函数(如果不需要):exec,system,passthru,shell_exec,popen,proc_open(命令执行)eval,assert(代码执行)file_put_contents,fputs,fwrite(结合路径遍历可能写 shell)phpinfo(信息泄露)error_reporting(生产环境应关闭显示错误)
错误处理
- 生产环境:
display_errors = Off,log_errors = On。 - 使用异常:用
try/catch捕获错误,返回统一的 JSON 或错误页面,避免泄露路径和 SQL 语句。
会话安全
- Session ID:使用
session_regenerate_id(true)在登录后重新生成。 - 超时:设置较短的 Session 过期时间,并结合
session.gc_maxlifetime。 - Httponly + Secure + SameSite:Cookie 设置如上。
数据库权限最小化
- 不要用
root连接数据库。 - 创建专门的应用账号,只授予
SELECT,INSERT,UPDATE,DELETE权限,严禁赋予FILE,CREATE,DROP,ALTER权限。
代码架构层面的设计
业务逻辑安全
- RABC(基于角色的访问控制):不要只在前端或 Session 里判断角色,要在后端每步操作都校验。
- 限额与限流:对“发送验证码”、“登入尝试”、“API 调用”进行频率限制,防止暴力破解。
- 幂等性:支付、提现等操作,使用唯一请求 ID 防止重复提交。
依赖管理
- Composer:定期运行
composer audit检查第三方包是否存在已知漏洞(CVE)。 - 更新:保持 PHP 版本和框架版本为最新的安全稳定版。
检查清单(快速自查)
| 攻击类型 | 检查要点 | 常用函数/配置 |
|---|---|---|
| SQL 注入 | 是否使用了 Prepared Statements? | PDO, MySQLi |
| XSS | 输出到 HTML 是否使用了 htmlspecialchars? | htmlspecialchars(), strip_tags() |
| CSRF | 操作是否校验 Token? | $_SESSION['token'] 比较 |
| 命令注入 | 是否禁用了 shell_exec 等函数? | escapeshellarg(), disable_functions |
| 文件上传 | 是否检查了文件魔数和重命名? | finfo_file(), move_uploaded_file() |
| 信息泄露 | 是否关闭了 display_errors? | ini_set('display_errors', 'Off') |
| 路径遍历 | 是否存在 的拼接? | realpath(), basename() |
| 代码执行 | 是否使用了 eval/assert? | 避免使用。 |
| 目录遍历 | 是否过滤了文件名中的 ? | basename() + 白名单 |
最核心的一句话是:输入视为有害,输出必有转义,逻辑必有校验,配置遵循最小化。 在这之上,配合现代的框架(Laravel, Symfony, ThinkPHP 等),它们内置了 CSRF 保护、ORM 防注入、模板引擎自动转义(Blade, Twig),可以帮你减少大量低级漏洞,但业务逻辑和配置层面的安全,仍然需要你自己把控。