SQL 注入是目前影响最深远的 Web 漏洞之一:攻击者把用户输入拼进查询语句,让“你给的字符串”变成“数据库执行的代码”,从而绕过认证、拖走数据、甚至删掉整张表。命令注入则是同一思路的变体,只是目标从数据库换成了服务器操作系统。防御它们共享一句口诀:数据与代码分开,参数化优先。
第二幕的第一课从最“老牌”也最“经典”的注入讲起。上一幕我们强调输入不可信,这一课就是最典型的后果展示——不可信输入没被隔离,直接被拼进了可执行语句。看懂 SQL 注入,不只是为了防一条 SQL,而是为了建立整个注入家族(命令注入、LDAP 注入、XPath 注入、模板注入)的通用直觉。
假设登录查询是这样拼出来的:
SELECT * FROM users WHERE username = '用户输入' AND password = '用户输入';
注意前后两个单引号把输入包成了字符串常量。麻烦来了:如果输入本身就是 ',这个单引号会提前把 SQL 字符串截断,后面的字面量变成语法,用户输入就“逃逸”进了查询的执行域。攻击者只要输入 ' OR '1'='1,并把密码随便填,整条语句变成:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '任意串';
因为 '1'='1' 恒为真,WHERE 条件对整个表成立,查询返回所有用户。很多粗糙的登录代码“发现 ID 非空就当登录成功”,于是攻击者直接以第一个用户身份进门。这就是把“数据”放进“代码”位置的直接后果。
数据库引擎读到的只是最终那一串字符,它不关心哪个部分是程序员写的、哪个部分是用户输入的。只要你把输入拼进语句,用户输入的每个字符都有机会变成 SQL 语法。这就是“拼接注入”的本质:字符串逃逸,语法接管。
扩展到命令注入:如果服务端把用户输入拼进系统命令,例如 system("ping " + 用户输入),输入若为 127.0.0.1; rm -rf /,分号让后续部分成为新命令。同理,模板注入、XPath 注入、LDAP 注入都是用同一个“数据进城变成代码”的模式攻打不同的解析器。
注入家族按照“目标解析器”来分,谁替你解析这段输入,它就是目标所在层。

被攻破后的能力远不止“绕过登录”。利用 UNION 可以跨表合拼结果返回你想要的列;利用注释符 -- 可以截断语句尾部;利用延时函数可以借响应时间做字符逐位盲注,好处是即使页面不直接回显数据也能一点点把库里的内容导出来。若数据库账户权限太高,还可能调用系统命令——这时注入就从“读数据”升级为“拿服务器”。
OWASP 常年把注入列入最常见漏洞前列并非偶然:它门槛低、危险大、又容易因为一句图省事的字符串拼接卷土重来。这也提示一个防守心态——不要抱着“我们输入是内部用的所以安全”的侥幸,任何拼接进语句的值都要按不可信处理,这在第一幕「输入越界」那节的结论里已经种下。
还有一个常在真实事故里出现的后手:注入不一定立刻被发现,很多拖库在数周后才被第三方监测或外部通报戳穿。给查询加一句显式超时、给异常记结构化日志,能显著缩短“已被注入却不自知”的时间窗,把风险的时间系数压制住。
# 参数化写法:语句骨架与参数分离 import sqlite3 cur = conn.cursor() cur.execute( "SELECT * FROM users WHERE username = ? AND password = ?", (username, password) # 只当值传入,绝不拼进字符串 )
# 危险写法:字符串拼接, 参数里带单引号即可逃逸 cur.execute("SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'")
⚠️ 常见坑:很多人以为“我字符串里加了关键词过滤就安全”,于是过滤
select、union。但SeLeCt大小写混合、注释分隔、URL 编码、Null 字节等手段都能绕过单词过滤。参数化是从根本断掉这条路的唯一稳妥方案。
背景:把某搜索接口输入作为动态拼接字段的查询改成参数化。操作:先在本地用 sqlite 建一张 users 表,写一个接受输入的查询函数。结果:先让拼接版本读入 ' OR 1=1 -- ,它会返回整表;再切换成参数化版本,同样输入只返回零行(参数化不改变语义)。解读:同一个载荷,拼接版因引号逃逸改变了查询语法,参数化版把载荷当字面量处理,天然免疫。变式:把目标换成命令执行场景,用 ; ls 观察拼接命令的执行与参数化无关的失效方式,验证“不同解析器、同一原理”的结论。