4. 输出编码与注入防御
注入类漏洞的本质,是程序把“用户可控的数据”错误地当成了“代码或指令”来执行。要真正防住它,不能只靠过滤几个关键字,而要从数据与代码的边界入手:让数据永远停留在数据的位置,让结构由程序预先固定。本节围绕 SQL 注入、命令注入、LDAP 注入三类典型场景,讲清原理、给出可落地的防御手段,并说明参数化查询、ORM 安全使用与输出编码的边界。
一、SQL 注入:拼接字符串是根源
SQL 注入发生在 SQL 语句由字符串拼接而成时。假设登录校验写成:
sql = "SELECT id FROM users WHERE name='" + name + "' AND pwd='" + pwd + "'"
当用户输入 name = admin' -- 时,语句变成 SELECT id FROM users WHERE name='admin' --' AND pwd='...',-- 之后被注释,密码校验被绕过。若输入 ' OR '1'='1,条件恒真,可能直接返回全部用户。这就是“数据越界成为语法”的典型后果。
防御的核心是参数化查询(预编译语句):SQL 结构先固定,参数以占位符传入,数据库把参数当作纯数据,不再参与语法解析。
cursor.execute(
"SELECT id FROM users WHERE name=%s AND pwd=%s",
(name, pwd)
)
注意几个常见误区:表名、列名、ORDER BY 的方向不能用占位符绑定,只能通过白名单映射,例如把 asc/desc 映射为固定字符串;LIKE 的通配符要由程序拼接在参数值上,而不是拼进 SQL 结构;存储过程内部若仍用字符串拼接,同样会注入。
二、命令注入:把参数交给 shell 就危险
命令注入发生在程序调用系统命令时。例如:
os.system("ping -c 1 " + ip)
若 ip = 127.0.0.1; cat /etc/passwd,分号让第二条命令被执行。防御思路有三层:第一,尽量不调用 shell,使用参数数组形式,让参数不经过 shell 解析:
subprocess.run(["ping", "-c", "1", ip], shell=False)
第二,对输入做严格白名单校验,例如 IP 只允许数字和点,或直接用 ipaddress.ip_address() 解析,解析失败即拒绝。第三,遵循最小权限,运行服务的账户不应有读取敏感文件或执行高危命令的权限。需要特别提醒:shell=True、反引号、$()、管道、重定向、换行符都是命令注入的常见入口,不能仅靠转义个别字符来兜底。
三、LDAP 注入:过滤器语法同样会被污染
LDAP 查询使用过滤器语法,如 (uid=alice)(userPassword=xxx)。若直接拼接:
filter = "(&(uid=" + uid + ")(userPassword=" + pwd + "))"
输入 uid = *)(uid=*))(|(uid=* 可改变过滤器逻辑,导致认证绕过或信息泄露。防御要点:对进入过滤器的特殊字符 * ( ) \ NUL 做转义(RFC 4515 转义规则,如 * 转 \2a),并优先使用语言提供的 LDAP 转义函数;同时限制查询返回的属性,避免把密码等敏感字段作为可查询条件。
四、ORM 安全使用:方便不等于自动安全
ORM 默认使用参数绑定,能挡住大部分 SQL 注入,但仍有几个高危用法需要警惕。第一,原生 SQL 拼接:Model.objects.raw("... WHERE id=" + uid) 会重新引入注入,应改为 raw("... WHERE id=%s", [uid])。第二,把用户输入直接作为字段名或排序字段,例如 order_by(request.GET['sort']),应通过白名单映射。第三,extra()、filter(**{user_key: value}) 这类动态关键字参数,若键来自用户输入,可能被用来构造意外查询。第四,批量操作与 Q 对象拼接时,确保值仍走参数绑定。
一个实用的判断标准是:凡是“结构部分”(表、列、排序方向、操作符)来自用户输入,就必须白名单;凡是“值部分”来自用户输入,就必须参数绑定。两者不能混用同一种处理方式。
五、输出编码:防御的另一半
注入防御不只是“输入侧”。当数据被输出到 HTML、JS、URL、CSS 等不同上下文时,需要按上下文做输出编码,否则会形成 XSS 等注入。原则是:HTML 文本用 HTML 实体编码,属性值加引号并编码,URL 参数用 encodeURIComponent,JS 字符串用 JSON 序列化或专用编码,CSS 用 CSS 转义。不要用一套“通用过滤”应付所有上下文,也不要在编码后再做反转义。
六、可落地的检查清单
| 场景 | 危险写法 | 安全写法 |
|---|---|---|
| SQL 查询 | 字符串拼接 SQL | 占位符参数绑定 |
| 排序/表名 | 直接拼用户输入 | 白名单映射 |
| 系统命令 | os.system 拼接 | 参数数组 + shell=False |
| IP 校验 | 正则宽松匹配 | ipaddress 解析 |
| LDAP 过滤 | 直接拼接 uid | RFC 4515 转义 |
| HTML 输出 | 原样输出用户数据 | 按上下文编码 |
最后强调工程习惯:把参数化查询、命令参数数组、LDAP 转义封装成团队统一的工具函数或代码模板,配合静态扫描(如检查 os.system、字符串拼接 SQL 的模式)和代码评审清单,才能让防御从“知道”变成“默认做到”。注入漏洞很少因为技术太难而出现,更多是因为图省事拼接了一次字符串。