文章总结: 本文面向PHP初学者阐述安全开发三大起点:用户输入不可信需划清信任边界;SQL注入源于拼接字符串,应使用PDO预处理分离指令与数据;XSS源于直接输出变量,需按输出上下文转义。强调安全开发是写代码时避免漏洞而非事后修补,并引用OWASPTop10佐证。 综合评分: 85 文章分类: WEB安全,安全开发,代码审计
第一行 PHP 就埋雷:安全开发的三个起点
原创
Sink Sink
船山信安
2026年10月4日 19:06 湖南
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
安全开发 · SECDEV
第一行 PHP 就埋雷:安全开发的三个起点
安全开发 · 写给刚学会 echo 就想上线的你
安全开发的第一课,不是学一个叫 htmlspecialchars 的函数,也不是背熟 PDO 怎么拼。第一课是承认一件事:你写的第一行 PHP,大概率已经给攻击者留了一个开口。这篇写给刚学会 echo、正打算把作业项目往线上推的人。你不需要先懂密码学,也不用读完一厚本安全著作,先建立几根底线就行。下面三个起点,是三个一上手就会踩的坑。每一个我都讲清它为什么危险,以及正确的写法长什么样。看完你未必能写出铁桶一样的系统,但至少不会再在第一行就埋雷。
01用户输入永远不可信
你写的第一个 PHP 页面,八成已经埋了雷。
这不是吓唬。大部分入门教程里第一段能跑起来的代码,就已经把外部数据直接用了,连想都没想。
PHP 给你准备了一组超全局变量:$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER。名字听着像系统自动替你填好的,其实每一个都来自请求方。
地址栏后面跟的 ?id=1,是用户敲的。表单里的字段,是用户填的。Cookie 存在用户浏览器里,意味着用户也能改。请求头里那些看似系统级的字段,不少同样能被客户端伪造。$_SERVER 里有一部分,也直接来自这次请求。
还有一个容易忽略的:$_REQUEST 把 GET、POST、COOKIE 三处合并了。你以为自己在读表单,攻击者却从 URL 里塞了同样名字的参数,覆盖顺序一乱,读到的值就不是你想的那个。把 $_REQUEST 当成”万能取参”用,是给混乱开门。
别以为只有表单和 URL 危险。文件上传时用户给的文件名,能带着路径符号;请求头里的 X-Forwarded-For 常被直接写进日志,也能夹带内容。凡是”来自请求”的,都是外部输入,哪怕它长得再像系统字段。
举个具体的例子。你写 $q = $_GET[‘q’]; 然后 SELECT … LIKE ‘%$q%’,用户传 q=% 或 q=_,在某些写法下就能把 LIKE 的语法撬开,匹配出本不该看到的内容。一个百分号、一个下划线,在 LIKE 里都是通配符。这就是”来自请求”四个字的分量。
它们全部位于你的信任边界之外。
信任边界,是我希望你记住的第一个词。它是一道界线:界线里面是你能控制的代码逻辑,界线外面是任何人都能往里塞东西的地方。凡是跨过这道线进来的数据,一律按”敌人给的”对待。
一个直观的办法:在脑子里给项目画一条线。线内的变量,是你自己赋值、自己保证的;线外进来的,先假设它可能是恶意的,再决定怎么用。这条线画清楚,后面两个起点才有地方落。
我早年的代码,现在回头看就是教学反面教材。登录之后,我从 Cookie 里读 user_id,拿去查这个人的订单列表,中间没有任何类型判断,也没有任何”这人到底是不是本人”的核对。Cookie 是用户能改的东西,改一个数字,订单就成了别人的。
埋雷的不是某个函数,是”这些数据是系统给的”这个错觉。
02SQL 注入:拼接字符串的危险
最典型的埋雷动作,是字符串拼接。
你用双引号字符串,把 $_GET[‘id’] 直接拼进一条 SELECT。本意是查 id 为 1 的那一行。攻击者传 id=1 OR 1=1,拼出来的条件永远为真,整张表都被拖出来。更激进的写法,还能在后面接上第二条语句,把别的数据也读走,甚至改写。
危险的根源不在”用了双引号”,在于你把”数据”和”指令”混进了同一个字符串。数据库拿到的是一个已经拼好的 SQL,它没法判断哪一段是你写的命令、哪一段是用户塞进来的值。在它眼里,那整句都是命令。
我见过有人用 mysql_query 配一串拼接,跑得挺欢,直到某天日志里出现一条不属于任何正常业务的查询。那一刻才明白,那个字符串是谁都能往里填的。
拼接的隐患还不止拖库。如果错误信息直接回显到页面,攻击者能靠报错一点点拼出你的表名、字段名。这一步一步,都是从”我把用户输入当成了 SQL 语法”开始的。
有人会说,那我加个判断,id 是数字才查,不就行了?思路对,但只救了这一处。项目里要拼的绝不止 id 一个字段,字符串、日期、枚举到处都是。靠每次手写判断兜,迟早漏一个。
真正稳的办法,是从写法上消灭拼接,而不是每次手动补洞。拼接之所以危险,是因为它把”能不能放心用”这个责任,推给了写代码的你每一秒的注意力。人靠不住,机制才靠得住。
// 危险写法:把用户输入直接拼进 SQL 字符串
$id = $_GET[‘id’];
$sql = “SELECT * FROM articles WHERE id = $id”;
$result = mysql_query($sql);
mysql_query 这套早该进了历史博物馆,但今天还有人照抄。问题一眼可见:id 连同它的内容,没经过任何处理就并入了命令。攻击者只要在那条值里动点手脚,拼出来的就不再是你想查的那句。
顺便说一个认知偏差:很多人觉得”我的网站没人看,不会被盯上”。攻击不是人一个个翻你的站,是脚本在扫整个网段。你上线那一分钟,自动化工具就已经把你的参数试了一遍。有没有人专门盯着你是一回事,有没有机器顺手试你,是另一回事。
03参数化查询:PDO 的正确写法
正确做法是把”值”从”指令”里拆出来,让两者各回各家。
PDO 的预处理语句干的就是这件事。你先写一条带占位符的 SQL,占位符用问号 ? 或者命名形式 :id 表示;之后用 execute,把参数作为一个数组传进去。数据库会先把 SQL 的结构定下来,再把你给的值安全地填到该填的位置——值这一辈子都成不了指令。
占位符有两种写法。? 简单,按位置传;:id 这种命名形式,在一条语句里出现多次同一个值时更清楚,也不用数第几个问号对应第几个变量。选哪种都行,关键是”用占位符,而不是拼字符串”。
这里有个常见误会,我踩过。有人以为 mysqli_real_escape_string 是万能药。它不是。它只是把引号、反斜杠之类转义掉,本质是”事后打补丁”,既不约束类型,也不理解上下文。一个数字型字段,你传个字符串进去,它照样帮你转义后塞进查询。转义不等于分离。
预处理是结构化地分离指令与数据,不是事后补救。这是两回事。
还有个细节容易被忽略:PDO 在某些数据库驱动下,默认会”模拟预处理”,也就是先在 PHP 端把值拼回字符串再发过去。你以为分开了,其实又拼了。保险起见,建连接时把 PDO::ATTR_EMULATE_PREPARES 设为 false,让数据库真正去做参数绑定。
也别觉得只有登录那一句才要预处理。列表、详情、统计、后台,凡是碰数据库的地方,一律走预处理。安全最怕”大多数时候都做了,就这一处图省事”——攻击者找的就是那一处。
预处理也不是银弹,它只解决”值”这一层。要是你把整段 SQL 的关键字也交给用户拼,比如让用户选查哪张表,那预处理管不到结构。记住:占位符管值,白名单管结构,两者别混为一谈。
如果一条 SQL 里既要有占位符、又想拼一点动态的东西,比如按用户选择切换排序字段,那部分绝不能也用字符串拼。排序字段、表名这类”结构”性的东西,要用白名单写死,绝不让用户输入决定。
// 正确写法:PDO 预处理,值与指令分离
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟,交给数据库真正绑定
]);
// 占位符用 ? 表示,参数通过 execute 的数组传入
$stmt = $pdo->prepare(‘SELECT * FROM articles WHERE id = ?’);
$stmt->execute([(int) $_GET[‘id’]]); // 值由驱动绑定,成不了 SQL 指令
$row = $stmt->fetch();
即便用了预处理,对整型字段多加一道 (int) 强制转换也不亏。防御是分层的,别非此即彼:预处理挡住注入,类型转换挡住”传了个数组进来”这类边界情况。
04XSS:直接 echo 变量的危险
第二个常见的雷,长得更无害,危害却一点不小。
你在模板里写 echo $name,本意只是把用户名显示出来。可如果这个名字里夹着一段
-->









评论