应使用ngx.var.request_uri手动提取未解码的原始查询字符串,禁用pcre utf-8模式,利用shared dict实现带ttl的ip黑名单,并在ngx.exit(403)前调用ngx.flush()。

OpenResty里用ngx.var.args直接拿不到完整原始查询参数
很多人一上来就写 ngx.var.args 想提取全部 query string,结果发现它已经被 Nginx 自动 decode 过、空格变+、%20被转成空格、甚至有些编码异常的参数直接丢弃。SQL注入检测必须基于原始字节流,否则绕过太容易。
正确做法是用 ngx.req.get_uri_args() 配合 ngx.var.request_uri 手动切分:
local uri = ngx.var.request_uri local query_start = string.find(uri, "?") local raw_query = query_start and uri:sub(query_start + 1) or ""
-
raw_query是未解码的原始字符串,可直接做正则匹配或字节扫描 - 别用
ngx.req.read_body()去读 POST body——除非你确定只拦截 GET 请求;POST 场景需额外判断content-type并调用ngx.req.get_post_args()或ngx.req.get_body_data() - 注意:如果 upstream 已经用
proxy_pass转发,且没加proxy_set_header X-Original-URI $request_uri;,原始 URI 在后端可能已不可见
用 PCRE 正则匹配 SQL 注入特征时必须禁用 UTF-8 模式
OpenResty 默认的 Lua PCRE(通过 string.match 或 ngx.re.match)在启用 UTF-8 模式下会把多字节序列当 Unicode 字符处理,导致 %27(单引号 URL 编码)被拆成 %、2、7 三段,规则完全失效。
务必显式关闭 UTF-8 模式:
local ok, err = ngx.re.match(raw_query, [[\b(SELECT|UNION|INSERT|UPDATE|DELETE)\b]], "jo")
-
"jo"中的o表示 “optimize once”,j表示 “disable JIT”,关键是没有u(UTF-8 mode) - 避免用
string.find做注入检测——它不支持 PCRE,无法写\b单词边界,容易误报(如匹配到user_id=123union中的 union) - 常见漏掉的 payload:
%2527(%27 的二次编码)、/**/注释绕过、SEL/**/ECT拆分关键字,这些得靠多层 decode + 多轮正则覆盖
动态黑名单不能只存内存,得用 shared dict + 过期时间防爆
有人把 IP 黑名单存在 Lua table 里,重启就清空,或者用 os.time() 手动遍历清理,结果 shared dict 内存涨满、worker 进程 OOM。
必须用 OpenResty 内置的 shared dict 并设 TTL:
lua_shared_dict sql_blacklist 10m;
init_by_lua_block {
local dict = ngx.shared.sql_blacklist
-- 可选:预热常用黑名单项,但不要 dump 全量
}
- 封禁逻辑里用
dict:set(ip, true, 300)(5 分钟),自动过期,不用手动删 - 检查是否在黑名单用
dict:get(ip) ~= nil,别用== true——因为set成功返回1,不是布尔值 - 高频请求下,别在每个请求都
set,先get判断是否存在,再决定是否set,减少 dict 锁竞争
ngx.exit(403) 前必须调用 ngx.flush() 否则客户端收不到响应
这是最常被忽略的细节:Lua 脚本里执行 ngx.exit(403) 时,如果之前有 ngx.say() 或 ngx.print() 输出了部分响应体,而 buffer 没刷出去,Nginx 可能直接终止连接,客户端看到的是空响应或超时。
- 所有拦截逻辑结尾统一写:
ngx.flush(true); ngx.exit(403) - 不要依赖
return或break退出——OpenResty 的执行阶段模型里,只有ngx.exit能真正中断当前请求生命周期 - 如果用了
access_by_lua_file,确保该配置块在proxy_pass之前;若放在content_by_lua_file就晚了,后端可能已经执行完 SQL
真实环境里,SQL 注入特征库和解码层数要根据业务协议深度定制,比如 GraphQL 查询、JSON POST body 里的嵌套字段、带签名的加密参数,这些都不能靠一层 raw_query 匹配解决。边缘拦截只是第一道筛子,别把它当成银弹。











