一、常见SQL注入方式
1.1 内联SQL注入(In-Band SQLi):
这是最常见的注入类型,攻击者通过同一通信渠道来注入SQL代码并获取结果。
例子:
比如一个登录表单,攻击者在用户名字段输入 admin' --,如果后端SQL语句是
`
则实际执行的SQL语句变成了
这里 -- 后面的内容被注释掉,因此SQL语句实际上忽略了密码检查。 再或者如下语句:
如果用户输入 p' OR 1=1 --,则实际执行的SQL语句变成了
预防措施:
- 严格检查输入变量的类型和格式(费力不讨好)
- 过滤和转义特殊字符
- 始终使用参数化查询,无论数据来源是用户输入还是数据库
此时,数据库驱动会将 username 和 password 作为纯数据值传入,不会被解释为 SQL 代码。即使输入是 admin' --,它也只是被当作一个字符串字面量.
1.2 基于错误的SQL注入(Error-based SQLi):
例子:在 PostgreSQL 中模拟基于错误的 SQL 注入 假设存在一个易受攻击的查询:
攻击者在输入框中提交: 1'::int; SELECT CAST((SELECT version()) AS int) --
或者更典型的写法(适用于数字型参数):1' AND 1=CAST((SELECT tablename FROM pg_tables LIMIT 1) AS int) --
如果后端将执行错误直接返回给前端,那么攻击者就可以获取到有用的信息,从而进一步攻击数据库。
例如上面的例子,数据库会抛出异常:
invalid input syntax for type integer: "PostgreSQL 17.7 on x86_64-pc-linux-musl, compiled by gcc (Alpine 14.2.0) 14.2.0, 64-bit"
CodeBlock Loading...invalid input syntax for type integer: "users"
预防措施:
- 禁止将数据库错误详情暴露给用户:使用通用错误页面
- 始终使用参数化查询(Prepared Statements),避免拼接 SQL
- 最小权限原则:Web 应用连接数据库的账户不应有访问 pgtables、informationschema 等系统表的权限
- 输入验证与过滤:对 ID 等字段进行类型校验(如只允许数字)
1.3 基于内容的盲SQL注入(Content-based Blind SQLi)
这是盲注(Blind SQL Injection)的一种形式,攻击者无法直接看到数据库返回的数据或错误信息,但可以通过观察页面内容、响应长度或特定文本是否存在等间接方式,判断构造的 SQL 条件是否为真,从而逐位推断数据库中的信息。
与内联注入不同,盲注不依赖数据库回显数据,而是通过"是/否"行为差异进行推理。
例子 假设一个在线商店提供商品搜索功能,用户在搜索框中输入关键词,后端执行如下查询:
正常情况下,输入 laptop 会返回包含"laptop"的商品;若无匹配,则显示"未找到商品"或空列表。
现在,攻击者怀疑数据库中存在名为 users 的表,希望验证其是否存在。于是,在搜索框中提交:
拼接后的实际 SQL 语句变为:
如果 users 表存在:子查询成功返回 'test',条件为真,主查询逻辑不变,页面可能正常显示部分商品(如包含字母 "a" 的商品)。 如果 users 表不存在:子查询报错或返回空,导致整个 WHERE 条件为假,页面显示"未找到商品"。 通过对比输入 a 和输入上述 payload 后的页面内容差异(例如是否有商品显示),攻击者即可判断 users 表是否存在。
更进一步,攻击者可利用此方法逐字猜解数据,例如:
若页面行为与正常搜索一致,说明 admin 用户密码的第一个字符是 'a';否则尝试 'b'、'c'……以此类推。
预防措施 始终使用参数化查询(Prepared Statements) 即使在盲注场景下,参数化也能确保用户输入被当作纯数据处理,不会影响 SQL 逻辑结构。
此时,无论输入 a' AND ... -- 还是任何特殊字符,都会被转义为字符串的一部分,最终等效于:
数据库会严格查找包含该完整字符串的商品名,不会执行额外逻辑。
1.4 基于布尔的盲注(Boolean-based Blind SQLi)
布尔盲注与内容盲注相似,但更常见的是通过布尔表达式的真/假来逐位推断数据,而非观察页面文本差异。攻击者发送不同的 payload,观察响应是否改变(比如返回空/非空、状态码或页面包含某元素与否)。
例子(PostgreSQL):
如果为真,页面表现可能与正常搜索一致;为假则表现不同。通过逐位判断,可以恢复敏感字段。
预防措施:
- 使用参数化查询,避免任何将输入直接拼接进 SQL 的场景
- 对关键字段在使用时再次校验,不要假设已存入数据库的数据就是“安全”的
1.5 时间盲注(Time-based Blind SQLi)
时间盲注通过让数据库在特定条件下延迟响应来传递“真/假”信息(常用 pg_sleep())。这在无法看到数据也无法看到错误信息时特别有用。
例子(PostgreSQL):
如果猜测为真,服务器响应会被延迟(例如延迟 5 秒),攻击者通过测量响应时间得知布尔结果,从而继续逐位猜解。
预防措施:
- 参数化查询
- 限制数据库可用函数和扩展(如禁止非必要的 sleep/外部命令)
- 在应用层限制响应时间并监控异常延迟请求
1.6 堆叠查询 / 补充查询(Stacked Queries / Piggybacking)
堆叠查询是指在一次请求中执行多条 SQL 语句(例如使用分号分隔)。如果数据库或驱动允许,攻击者可以在原始查询后追加任意语句(DROP、INSERT、UPDATE 等)。
例子:
注意:许多数据库驱动(包括多数安全配置下的 PostgreSQL 驱动)默认不允许在单次 execute 中执行多条语句,但某些环境或错误配置仍存在风险。
预防措施:
- 使用参数化查询并确保驱动/ORM 不允许多语句执行
- 最小权限:数据库账户不应有删除/修改权限(除非必要)
- 对 DDL/DML 操作进行额外的服务器端审计与确认流程
1.7 Out-of-Band (OOB) SQL 注入
OOB 注入利用数据库能够向外部通道(网络、DNS、文件系统等)发送数据的能力,将敏感信息泄露到攻击者可控的接收方。常见于不能直接回显结果且存在可触发外联函数或不受限扩展时。
示例场景(Postgres,需存在不受限扩展或高权限):
- 使用
COPY ... TO PROGRAM、dblink或不受限的 PL 语言来触发对外部服务器的请求,从而把数据发送给攻击者。
预防措施:
- 严格限制数据库扩展与 PL 语言的使用;非必要时禁用
COPY TO PROGRAM、dblink等 - 限制数据库用户为非超级用户,关闭不必要的功能
- 网络出口控制(Egress)和监控,检测异常外部连接
1.8 第二阶 SQL 注入(Second-order SQLi)
第二阶注入发生在用户输入最初被安全地存储(看起来无害),但后续在另一个上下文中再次被拼接成 SQL 并执行时。例如:用户在注册时输入某段文本存入数据库,后来管理员界面把这段文本直接拼接进动态查询或 SQL 片段,从而触发注入。
示例:
- 用户注册时输入:
O'Reilly(安全地被参数化存储) - 但管理员面板以字符串拼接方式构造动态 SQL:
'SELECT * FROM docs WHERE author = ''' + db_value + '''',这里的 db_value 来自数据库,导致注入发生。
预防措施:
- 永远在“使用”数据时再次进行参数化和必要的转义;不要假设已存储的数据就是安全的
- 避免在数据库中存储已经包含 SQL 片段的用户输入并在未处理的情况下再次执行
1.9 存储过程 / 函数中的动态 SQL 注入
当存储过程或函数内部使用动态 SQL(例如通过 EXECUTE 拼接字符串)并包含未受信任的输入时,会发生注入。PostgreSQL 的 plpgsql 提供 EXECUTE、format() 等,需要谨慎使用。
示例(危险用法):
更安全的写法(使用 USING 或 format + %L):
预防措施:
- 在存储过程里使用
EXECUTE ... USING或format(..., %L/%I)做好字面量/标识符引用 - 避免把用户输入直接拼接到动态 SQL 中
- 对需要动态构建的 SQL 进行严格白名单控制
二、结语
记住下面 6 个要点,就能挡掉绝大多数注入攻击:
- 永远参数化:Prepared Statements / 占位符(不要手动拼接字符串)
- 最小权限:数据库账户只给必要权限,禁用危险扩展
- 不泄露错误:不要把数据库堆栈或详细错误展示给用户
- 限制输入:对用户输入进行长度、格式、类型、范围等限制
- 定期审计:开启审计、限流、WAF、定期渗透测试
- 安全编码:避免使用不安全的函数、库、扩展
三、参考
以上示例与防护建议均以 PostgreSQL 为测试背景;实际项目中请结合 ORM/驱动文档与运维策略实现最小变更的安全修复。