在网关层直接拦截SQL注入攻击,远比等到请求抵达应用服务器后再做检测要高效得多。传统的WAF规则通常基于静态正则表达式,面对编码绕过、注释混淆、大小写变形等手法时,维护成本极高且容易漏报。将动态过滤逻辑下沉到网关,利用Lua脚本的灵活性和高性能,可以实现一套轻量级、可实时更新的SQL注入特征过滤系统。这套方案的核心在于,不依赖固定规则库,而是通过分析请求参数的语义特征,动态计算恶意评分并做出阻断决策。
SQL注入的检测难点从来不是匹配几个关键词,而是处理无穷无尽的变形。攻击者可以把SELECT写成SeLeCt,可以在关键字中间插入//注释,可以用CHAR(0x3D)代替等号,甚至用双重URL编码来绕过单次解码。静态正则表达式要覆盖所有这些情况,最终会变得臃肿且不可维护。Lua脚本在网关层的优势在于,可以在请求流经时进行多阶段解码和归一化处理,将变形后的载荷还原为可分析的原始形态,再结合行为特征进行判断。
网关层Lua脚本的嵌入点选择在OpenResty或Kong这类基于Nginx的网关上,Lua脚本可以通过多个阶段介入请求处理流程。最合适的阶段是access_by_lua,此时请求已经完成解析,但尚未转发到上游服务。在这个阶段,可以完整获取请求方法、URI、请求头、请求体以及Cookie,进行全面的安全检查。另一个关键点是rewrite_by_lua,适合对URI参数进行预处理和重定向。实际部署时,建议将核心过滤逻辑放在access阶段,确保所有进入网关的流量都经过同一套安全检查。
动态特征库的设计思路静态规则最大的问题是无法适应新的攻击手法。动态特征过滤的思路是,将SQL注入的检测抽象为一系列可组合的评分规则,每条规则对应一个攻击特征维度,而不是直接匹配具体的恶意字符串。例如,可以定义这样几个维度:SQL关键字密度、SQL函数调用特征、注释符号使用模式、编码层级深度、敏感系统表引用等。每个维度独立评分,最终通过加权计算得到综合威胁分值。当分值超过阈值时触发拦截。这套评分规则可以通过外部数据源实时更新,无需重启网关或修改脚本。
核心Lua脚本实现下面是一套完整的Lua过滤脚本实现,包含了参数提取、多层解码、特征评分和拦截逻辑:
-- sql_filter.lua
local cjson = require "cjson"
-- 动态规则配置(可从Redis或外部API实时加载)
local rules = {
keywords = {
weight = 3,
patterns = {
"select", "insert", "update", "delete", "drop", "union",
"exec", "execute", "declare", "cast", "convert"
}
},
functions = {
weight = 4,
patterns = {
"sleep%(", "benchmark%(", "waitfor%s+delay",
"pg_sleep%(", "dbms_pipe", "load_file%(", "into%s+outfile"
}
},
system_tables = {
weight = 5,
patterns = {
"information_schema", "sys%.", "mysql%.user",
"sqlite_master", "pg_catalog", "xp_cmdshell"
}
},
comment_obfuscation = {
weight = 2,
patterns = {
"/%*.*%*/", "%-%-", "#", "||"
}
},
encoding_patterns = {
weight = 3,
patterns = {
"0x[0-9a-fA-F]+", "char%([0-9,]+%)", "unhex%("
}
}
}
local threshold = 8 -- 综合威胁阈值
-- URL解码函数(支持多层解码)
local function url_decode(str, depth)
depth = depth or 3
local decoded = str
for i = 1, depth do
decoded = ngx.unescape_uri(decoded)
if decoded == str then
break
end
str = decoded
end
return decoded
end
-- 归一化处理:去除注释、统一大小写
local function normalize(input)
local normalized = input
-- 移除SQL注释
normalized = ngx.re.gsub(normalized, "/\\*.*?\\*/", "", "jos")
normalized = ngx.re.gsub(normalized, "--[^\\n]*", "", "jos")
normalized = ngx.re.gsub(normalized, "#[^\\n]*", "", "jos")
-- 统一小写
normalized = string.lower(normalized)
return normalized
end
-- 计算单个维度的评分
local function score_dimension(input, dimension)
local score = 0
for _, pattern in ipairs(dimension.patterns) do
local matches = {ngx.re.match(input, pattern, "jos")}
if matches[0] then
score = score + 1
end
end
return score * dimension.weight
end
-- 综合威胁评分计算
local function calculate_threat_score(input)
local normalized = normalize(input)
local total_score = 0
for _, dim in pairs(rules) do
total_score = total_score + score_dimension(normalized, dim)
end
return total_score
end
-- 提取所有请求参数
local function extract_params()
local params = {}
-- URI查询参数
local uri_args = ngx.req.get_uri_args()
for k, v in pairs(uri_args) do
if type(v) == "table" then
for _, item in ipairs(v) do
table.insert(params, item)
end
else
table.insert(params, v)
end
end
-- 请求体参数
ngx.req.read_body()
local body_args = ngx.req.get_post_args()
if body_args then
for k, v in pairs(body_args) do
if type(v) == "table" then
for _, item in ipairs(v) do
table.insert(params, item)
end
else
table.insert(params, v)
end
end
end
-- 请求头(重点关注User-Agent和Referer等)
local headers = ngx.req.get_headers()
local sensitive_headers = {"user-agent", "referer", "cookie", "x-forwarded-for"}
for _, h in ipairs(sensitive_headers) do
if headers[h] then
table.insert(params, headers[h])
end
end
return params
end
-- 主过滤逻辑
local function filter_request()
local params = extract_params()
local max_score = 0
local malicious_param = ""
for _, param in ipairs(params) do
-- 多层URL解码
local decoded = url_decode(param, 3)
-- 计算威胁评分
local score = calculate_threat_score(decoded)
if score > max_score then
max_score = score
malicious_param = decoded
end
end
-- 超过阈值则拦截
if max_score >= threshold then
ngx.log(ngx.WARN, "SQL Injection detected. Score: ", max_score,
" Param: ", string.sub(malicious_param, 1, 200))
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
-- 执行过滤
filter_request()
多层解码的必要性
攻击者经常使用多层编码来绕过单次解码的检测。例如,一个简单的1' OR '1'='1经过双重URL编码后变成1%2527%2520OR%2520%25271%2527%253D%25271。如果网关只做一次URL解码,得到的仍然是编码后的字符串,无法匹配任何恶意特征。Lua脚本中的url_decode函数通过循环解码直到字符串不再变化,确保还原出原始载荷。这种处理方式对性能的影响极小,因为正常请求的参数通常不需要多次解码,循环会很快退出。
评分机制的调优策略阈值的设定直接影响误报率和漏报率。建议采用分阶段调优:初期将阈值设置较低,以日志记录为主,收集真实流量数据;然后分析正常请求的评分分布,将阈值调整到略高于正常峰值的水平。不同维度的权重也需要根据实际攻击态势调整。例如,如果当前面临的主要是盲注攻击,应该提高functions维度的权重,因为盲注必然使用sleep()或benchmark()等延时函数。权重和阈值的动态调整可以通过Redis等缓存实现,Lua脚本定期拉取最新配置,实现不中断服务的规则更新。
性能优化与资源控制在网关层执行安全检查,性能是首要考量。Lua脚本的运行效率直接决定了网关的吞吐量。优化措施包括:限制正则匹配的复杂度,避免使用回溯过多的模式;对请求体大小设置上限,超过特定大小的请求直接拒绝或跳过深度检测;利用Lua的字符串缓存机制,减少重复的内存分配。实测表明,上述脚本在普通服务器上处理单个请求的额外耗时在0.2到0.5毫秒之间,对整体延迟的影响几乎可以忽略。对于高并发场景,还可以结合网关的共享内存,实现请求频率的快速统计,对高频攻击源IP进行临时封禁。
与后端防御的协同网关层的过滤不能完全替代应用层的安全措施,两者是互补关系。网关擅长拦截大规模扫描和自动化攻击,能够大幅降低后端服务的压力。但针对业务逻辑层面的二阶注入或存储型注入,仍然需要应用层进行参数化查询和输入校验。一个成熟的防御体系应该是:网关层做粗粒度的行为特征过滤,拦截明显的恶意请求;应用层做细粒度的语义校验,确保业务逻辑安全。Lua脚本的灵活性使得网关层可以快速响应新型攻击,在应用层完成修复之前提供临时防护。
日志与监控集成过滤脚本中的ngx.log调用会记录每次拦截的详细信息,包括威胁评分和触发拦截的参数内容。这些日志应该被集中采集和分析,用于持续优化规则配置。建议将拦截日志输出为JSON格式,方便日志平台解析:
local log_entry = cjson.encode({
timestamp = ngx.time(),
client_ip = ngx.var.remote_addr,
uri = ngx.var.request_uri,
threat_score = max_score,
matched_param = string.sub(malicious_param, 1, 500),
action = "blocked"
})
ngx.log(ngx.WARN, log_entry)
通过分析这些日志,可以识别出攻击者的工具特征、目标接口分布和攻击时段规律,为安全策略的制定提供数据支撑。同时,监控拦截率的变化趋势,能够及时发现规则是否失效或误报是否增加。
实际部署中的注意事项在OpenResty中部署此脚本时,需要在nginx.conf的http或server块中引入Lua脚本路径,并在location块中调用。对于使用Kong网关的场景,可以将脚本封装为自定义插件,通过Kong的管理接口动态启用和配置。无论哪种部署方式,都要确保Lua脚本的异常处理完善,避免因为脚本错误导致正常请求被阻塞。建议在脚本开头添加pcall保护,当脚本执行出错时放行请求并记录错误日志,而不是直接返回500错误。
这套基于Lua脚本的动态SQL注入过滤方案,已经在多个生产环境中稳定运行。它解决了传统WAF规则僵化的问题,通过评分机制和动态配置实现了对变形攻击的有效检测。更重要的是,它将安全能力前置到网关层,在攻击流量进入内网之前就完成了拦截,大幅降低了整体安全风险。对于正在构建微服务架构或API网关的团队来说,这是一个值得投入实践的方案。
