下面给你一套网关自动拦截 + 更完整日志记录的落地方案,优先以 NGINX / OpenResty 为例,适合你们后续接 ELK、Loki、Splunk、Azure Monitor 或 SIEM。
一、网关中如何实现自动拦截?
1. 推荐拦截策略
自动拦截不要一上来全量阻断,建议分三档:
观察期:只记录日志,不拦截
灰度期:只拦截高风险 Path,不拦截普通 Query
强制期:高风险 Path + 高风险参数双重编码都拦截
Double Encoding 的风险在于攻击者可能把参数或路径编码两次,从而绕过只解码一次的安全过滤器;OWASP 明确提到它可用于绕过认证、访问控制和安全过滤,典型场景包括路径穿越和 XSS。 [rfc-editor.org]
2. 高风险路径拦截规则
建议优先拦截这些模式:
%252e%252e%252f 双重编码的 ../
%252e%252e%255c 双重编码的 ..\
%252f 双重编码的 /
%255c 双重编码的 \
%253c 双重编码的 <
%253e 双重编码的 >
其中 %252e%252e%252f 解码一次是 %2e%2e%2f,再解码才是 ../,这是典型路径穿越绕过形式。OWASP 示例中也说明 ../ 可先编码为 %2E%2E%2F,再编码为 %252E%252E%252F。 [rfc-editor.org]
二、NGINX 原生自动拦截配置
适合先快速落地。
http {
map $request_uri $block_double_encoded_path {
default 0;
# 双重编码的 ../
~*%252e%252e%252f 1;
~*%252e%252e%255c 1;
# 双重编码的 / 或 \
~*%252f 1;
~*%255c 1;
# 双重编码的 < >
~*%253c 1;
~*%253e 1;
}
server {
listen 80;
server_name api.example.com;
if ($block_double_encoded_path = 1) {
return 400;
}
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://backend;
}
}
}
说明:
$request_uri更适合检测原始请求,因为它保留原始 URI 和 QueryString。$uri是 NGINX 当前处理的 URI,可能经过规范化、内部重写或路径处理。- 排查编码问题时建议同时记录
$request_uri和$uri。 [nodejs.cn], [baeldung-cn.com]
三、NGINX 记录更多日志字段
NGINX 的 ngx_http_log_module 支持通过 log_format 自定义日志格式,并通过 access_log 指定输出文件;日志可以配置多个格式,也可以按条件输出。 [nginx.org]
1. 推荐 JSON 日志格式
http {
map $request_uri $has_percent25 {
default 0;
~*%25 1;
}
map $request_uri $has_double_encoded_path_risk {
default 0;
~*%252e%252e%252f 1;
~*%252e%252e%255c 1;
~*%252f 1;
~*%255c 1;
~*%253c 1;
~*%253e 1;
}
map $block_double_encoded_path $encoding_action {
default "allow";
1 "block";
}
log_format url_encoding_json escape=json
'{'
'"time":"$time_iso8601",'
'"request_id":"$request_id",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"method":"$request_method",'
'"scheme":"$scheme",'
'"host":"$host",'
'"server_name":"$server_name",'
'"server_addr":"$server_addr",'
'"request":"$request",'
'"request_uri":"$request_uri",'
'"uri":"$uri",'
'"args":"$args",'
'"query_string":"$query_string",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_length":$request_length,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"content_type":"$content_type",'
'"content_length":"$content_length",'
'"has_percent25":$has_percent25,'
'"has_double_encoded_path_risk":$has_double_encoded_path_risk,'
'"encoding_action":"$encoding_action"'
'}';
access_log /var/log/nginx/url_encoding_access.log url_encoding_json;
}
建议使用 escape=json,因为 User-Agent、Referer、URI 里可能包含引号、反斜杠、换行等字符,直接拼 JSON 容易破坏日志结构;NGINX 的 log_format 支持 escape 参数,官方文档也说明日志格式可自定义并由 access_log 使用。 [nginx.org]
2. 建议增加的日志字段
建议记录这些字段:
基础字段:
time
request_id
remote_addr
x_forwarded_for
method
scheme
host
server_name
server_addr
status
URI 字段:
request
request_uri
uri
args
query_string
性能字段:
request_time
upstream_response_time
upstream_addr
upstream_status
request_length
body_bytes_sent
安全字段:
has_percent25
has_double_encoded_path_risk
encoding_action
matched_pattern
risk_level
event_type
客户端字段:
user_agent
referer
content_type
content_length
业务透传字段:
x_app_id
x_tenant_id
x_user_id
x_sdk_name
x_sdk_version
x_trace_id
注意:敏感字段不要明文记录,例如 access_token、refresh_token、password、authorization、cookie、code、secret、signature。OWASP Logging Cheat Sheet 建议应用日志应服务于安全和运营目的,但要避免记录过多或记录敏感数据。 [mdn.org.cn]
四、OpenResty 实现更强自动拦截
如果需要识别具体命中的规则、区分 Path 和 Query、输出更丰富的安全日志,建议使用 OpenResty。
1. OpenResty 自动拦截 Path
server {
listen 80;
server_name api.example.com;
access_by_lua_block {
local cjson = require "cjson.safe"
local request_uri = ngx.var.request_uri or ""
local uri = ngx.var.uri or ""
local lower_request_uri = string.lower(request_uri)
local risk_patterns = {
{ name = "double_encoded_dot_dot_slash", pattern = "%%252e%%252e%%252f", risk = "critical" },
{ name = "double_encoded_dot_dot_backslash", pattern = "%%252e%%252e%%255c", risk = "critical" },
{ name = "double_encoded_slash", pattern = "%%252f", risk = "high" },
{ name = "double_encoded_backslash", pattern = "%%255c", risk = "high" },
{ name = "double_encoded_lt", pattern = "%%253c", risk = "medium" },
{ name = "double_encoded_gt", pattern = "%%253e", risk = "medium" }
}
for _, rule in ipairs(risk_patterns) do
if string.find(lower_request_uri, rule.pattern) then
local event = {
event_type = "url_encoding_anomaly",
event_subtype = "high_risk_double_encoded_path",
action = "block",
risk_level = rule.risk,
matched_rule = rule.name,
matched_pattern = rule.pattern,
request_id = ngx.var.request_id,
remote_addr = ngx.var.remote_addr,
x_forwarded_for = ngx.var.http_x_forwarded_for,
method = ngx.var.request_method,
host = ngx.var.host,
request_uri = request_uri,
uri = uri,
user_agent = ngx.var.http_user_agent
}
ngx.log(ngx.WARN, cjson.encode(event))
ngx.status = 400
ngx.say("Bad Request")
return ngx.exit(400)
end
end
}
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://backend;
}
}
这类策略适合直接拦截路径穿越、路径混淆、双重编码 slash/backslash 等风险,因为这些模式常用于绕过安全检查。OWASP 和安全厂商文档都指出 Double URL Encoding 可作为规避技术,常见于目录遍历、XSS、SQL 注入和访问控制绕过。 [rfc-editor.org], [rfc2cn.com]
2. OpenResty 拦截高风险 Query 参数
建议对这些参数启用严格策略:
redirect_uri
returnUrl
next
callback
url
target
continue
path
file
download
resource
配置示例:
server {
listen 80;
server_name api.example.com;
access_by_lua_block {
local cjson = require "cjson.safe"
local high_risk_params = {
redirect_uri = true,
returnUrl = true,
next = true,
callback = true,
url = true,
target = true,
continue = true,
path = true,
file = true,
download = true,
resource = true
}
local function has_encoded_pattern(s)
return type(s) == "string"
and string.find(s, "%%[0-9a-fA-F][0-9a-fA-F]") ~= nil
end
local function is_probably_double_encoded(s)
if type(s) ~= "string" then
return false
end
local decoded_once = ngx.unescape_uri(s)
if not decoded_once then
return true
end
return has_encoded_pattern(decoded_once)
end
local args = ngx.req.get_uri_args()
for name, value in pairs(args) do
if high_risk_params[name] then
local values = type(value) == "table" and value or { value }
for _, v in ipairs(values) do
if is_probably_double_encoded(v) then
local event = {
event_type = "url_encoding_anomaly",
event_subtype = "double_encoded_high_risk_param",
action = "block",
risk_level = "high",
param_name = name,
param_value_sample = string.sub(v, 1, 200),
request_id = ngx.var.request_id,
remote_addr = ngx.var.remote_addr,
x_forwarded_for = ngx.var.http_x_forwarded_for,
method = ngx.var.request_method,
host = ngx.var.host,
request_uri = ngx.var.request_uri,
uri = ngx.var.uri,
user_agent = ngx.var.http_user_agent
}
ngx.log(ngx.WARN, cjson.encode(event))
ngx.status = 400
ngx.say("Bad Request")
return ngx.exit(400)
end
end
end
end
}
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://backend;
}
}
判断逻辑是:解码一次后仍然包含 %XX,就认为疑似双重编码。这对应 Double Encoding 的典型绕过方式:一个安全组件只解码一次,而后端或其他模块又执行第二次解码,导致安全检查看到的内容和业务真正使用的内容不一致。 [rfc-editor.org]
五、OpenResty 记录更多日志字段
如果只用 NGINX access log,字段比较固定;如果用 OpenResty,可以写出非常详细的安全事件日志。
1. 安全事件日志函数
http {
lua_shared_dict encoding_security_events 20m;
server {
listen 80;
server_name api.example.com;
access_by_lua_block {
local cjson = require "cjson.safe"
local function safe_sample(value, max_len)
if not value then
return nil
end
value = tostring(value)
if string.len(value) > max_len then
return string.sub(value, 1, max_len) .. "..."
end
return value
end
local function write_security_log(event)
event.time = ngx.var.time_iso8601
event.request_id = ngx.var.request_id
event.remote_addr = ngx.var.remote_addr
event.x_forwarded_for = ngx.var.http_x_forwarded_for
event.method = ngx.var.request_method
event.scheme = ngx.var.scheme
event.host = ngx.var.host
event.server_name = ngx.var.server_name
event.request_uri = ngx.var.request_uri
event.uri = ngx.var.uri
event.args = safe_sample(ngx.var.args, 1000)
event.status = ngx.status
event.user_agent = ngx.var.http_user_agent
event.referer = ngx.var.http_referer
event.content_type = ngx.var.content_type
event.content_length = ngx.var.http_content_length
event.trace_id = ngx.var.http_x_trace_id
event.app_id = ngx.var.http_x_app_id
event.tenant_id = ngx.var.http_x_tenant_id
event.sdk_name = ngx.var.http_x_sdk_name
event.sdk_version = ngx.var.http_x_sdk_version
ngx.log(ngx.WARN, cjson.encode(event))
end
local request_uri = ngx.var.request_uri or ""
local lower_request_uri = string.lower(request_uri)
if string.find(lower_request_uri, "%%25") then
write_security_log({
event_type = "url_encoding_anomaly",
event_subtype = "percent25_detected",
action = "warn",
risk_level = "medium",
has_percent25 = true
})
end
if string.find(lower_request_uri, "%%252e%%252e%%252f") then
write_security_log({
event_type = "url_encoding_anomaly",
event_subtype = "double_encoded_path_traversal",
action = "block",
risk_level = "critical",
matched_pattern = "%252e%252e%252f"
})
ngx.status = 400
ngx.say("Bad Request")
return ngx.exit(400)
end
}
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://backend;
}
}
}
2. 建议记录的扩展字段
建议最终日志结构包含:
{
"time": "2026-07-04T11:10:00+08:00",
"event_type": "url_encoding_anomaly",
"event_subtype": "double_encoded_path_traversal",
"action": "block",
"risk_level": "critical",
"matched_pattern": "%252e%252e%252f",
"request_id": "xxxx",
"trace_id": "xxxx",
"remote_addr": "10.1.2.3",
"x_forwarded_for": "1.2.3.4",
"method": "GET",
"scheme": "https",
"host": "api.example.com",
"server_name": "api.example.com",
"request_uri": "/download/%252e%252e%252fetc/passwd",
"uri": "/download/%2e%2e%2fetc/passwd",
"args": "file=%252e%252e%252fetc%252fpasswd",
"status": 400,
"user_agent": "curl/8.0",
"referer": "-",
"content_type": "-",
"content_length": "-",
"app_id": "portal",
"tenant_id": "mdl",
"sdk_name": "mdl-web-sdk",
"sdk_version": "1.2.3"
}
这类结构化日志便于后续按 event_type、event_subtype、risk_level、action、sdk_version、client_ip 聚合分析。OWASP Logging Cheat Sheet 也建议应用日志保持一致,便于跨系统采集、关联、分析和管理。 [mdn.org.cn]
六、同时输出 Access Log 和 Security Log
建议分两个日志:
access log:所有请求都记录
security log:只记录异常/拦截/告警事件
1. Access Log
access_log /var/log/nginx/access_json.log url_encoding_json;
2. Security Log
OpenResty 用:
ngx.log(ngx.WARN, cjson.encode(event))
如果要单独文件,可在 NGINX error log 中按级别收集,或者通过 sidecar / fluent-bit / filebeat 采集后按 event_type=url_encoding_anomaly 分流。
七、自动拦截上线建议
阶段 1:只记录,不拦截
# 命中后只写日志,不 return 400
观察:
1. 哪些服务最多
2. 哪些 SDK 版本最多
3. 哪些参数最多
4. 是否有业务合法误报
阶段 2:只拦截高风险 Path
先拦截:
%252e%252e%252f
%252e%252e%255c
%255c
谨慎处理:
%252f
因为某些业务可能把完整 URL 放到参数里,里面的 / 被双重编码可能造成误报。Thales 的文档也提到,Double URL Encoding 常作为规避技术,但 redirect URL 参数保存真实 URL 时可能出现误报,需要结合参数语义处理。 [rfc2cn.com]
阶段 3:高风险 Query 参数拦截
对以下字段启用:
redirect_uri
returnUrl
next
callback
path
file
download
url
target
策略:
解码一次后仍有 %XX => block
非法 URL 编码 => block
普通参数 => warn
八、测试命令
1. 应该被拦截:双重编码路径穿越
curl -i 'http://api.example.com/download/%252e%252e%252fetc/passwd'
预期:
HTTP/1.1 400 Bad Request
2. 应该被记录:包含 %25
curl -i 'http://api.example.com/search?q=Tom%2520Jerry'
预期:
可先放行,但日志中 has_percent25=1
3. 高风险参数双重编码应拦截
curl -i 'http://api.example.com/login?redirect_uri=https%253A%252F%252Fapp.example.com%252Fcallback'
预期:
HTTP/1.1 400 Bad Request
九、最终推荐配置组合
如果你们当前是普通 NGINX:
1. 先用 map + if 实现高风险路径自动拦截
2. access_log 用 JSON 格式记录更多字段
3. 后续接入 OpenResty 或 WAF 做更细粒度参数检测
如果可以用 OpenResty:
1. Path 风险直接 block
2. 高风险 Query 参数 decode once 后仍有 %XX 直接 block
3. 普通参数出现 %25 先 warn
4. 安全事件输出 JSON 日志
5. 日志中记录 request_id、sdk_name、sdk_version、matched_pattern、action
十、简短总结
自动拦截:
优先拦截 %252e%252e%252f、%252e%252e%255c、%255c、%253c、%253e 等高风险模式。
更多日志字段:
记录 request_uri、uri、args、has_percent25、matched_pattern、risk_level、action、request_id、sdk_name、sdk_version。
上线策略:
先观察,再灰度拦截,最后对高风险 Path 和高风险参数强制拦截。
浙公网安备 33010602011771号