必须源码编译并静态链接自定义模块,在NGX_HTTP_POST_READ_PHASE阶段遍历r->headers_in.headers原始链表读取未归一化header字节,禁用动态模块与load_module,通过--add-module指定路径编译。

如何在 Nginx 中加载自定义模块并确保其能读取原始请求头
必须从源码编译 Nginx,静态链接模块;动态模块(.so)无法访问未被 Nginx 显式解析或保留的原始 header 字段(比如大小写混合、重复字段、空格不规范的 X-Forwarded-For)。Nginx 默认会 normalize header 名(转小写+去空格),而审计场景常需原始字节流。
实操建议:
- 在
ngx_http_my_audit_module.c的ngx_http_my_audit_handler中,直接访问r->headers_in.headers链表,而非r->headers_in.XXX封装字段(如r->headers_in.host是已归一化的) - 用
ngx_list_part(&r->headers_in.headers)遍历所有原始 header 节点,每个节点的key.data和value.data是原始未解码字节 - 编译时加
--add-module=/path/to/your/module,不能用load_module指令
怎样识别并拦截非法 HTTP 头名(如含控制字符、非 ASCII、双下划线)
Nginx 本身在解析阶段就会丢弃含非法字符的 header(如 \x00、\r、\n),但攻击者可能绕过 parser 直接发 raw packet 到 upstream 或利用某些版本的解析漏洞。因此审计模块必须在 NGX_HTTP_POST_READ_PHASE 阶段介入,早于 header 解析完成。
实操建议:
- 检查每个
header->key.len > 0且header->key.data中每个字节满足:c >= 0x21 && c (即可见 ASCII,排除引号、井号、反斜杠等) - 特别拦截含
__(双下划线)、X-Api-Key后跟空格、Content-Length与Transfer-Encoding同时存在等典型异常组合 - 匹配到即调用
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST),不要用return跳过后续 phase
如何实时提取并判断异常访问行为(如高频 UA 变更、伪造 Referer、空 User-Agent)
行为分析必须结合请求上下文,不能只看单个 header。Nginx 默认不维护跨请求状态,所以需借助共享内存(ngx_shm_zone_t)+ slab 分配器实现轻量级滑动窗口计数,避免依赖外部存储引入延迟。
实操建议:
- 为每个 client IP + User-Agent 的 SHA256 前 8 字节构造 key,存入
ngx_slab_alloc分配的 slot,记录最近 60 秒内请求数(用ngx_current_msec做时间戳比对) - 若
r->headers_in.user_agent == NULL或其value.len == 0,直接拦截;若r->headers_in.referer != NULL但 host 部分不匹配server_name且非白名单域名,记为可疑 - 注意:不要在
NGX_HTTP_ACCESS_PHASE之后做 heavy 计算,否则阻塞 worker;计数逻辑应放在NGX_HTTP_POST_READ_PHASE或自定义 phase
为什么 ngx_http_upstream_t 里看不到原始 header,以及怎么把审计结果透传给后端
一旦进入 upstream 阶段,Nginx 已将 r->headers_in 归一化并重构成 u->headers_in,原始字节信息丢失。若需向后端透传审计结论(如 X-Audit-Status: blocked),必须在 ngx_http_upstream_create_request 之前注入。
实操建议:
- 在模块的
ngx_http_my_audit_handler中,若判定为高危请求,设置r->upstream->conf->hide_headers不生效,改用ngx_table_elt_t *h = ngx_list_push(&r->upstream->headers_in.headers)手动添加 header - 更稳妥的做法是:用
ngx_http_set_builtin_header注入X-Audit-Result,并在proxy_set_header配置中显式声明透传,例如proxy_set_header X-Audit-Result $sent_http_x_audit_result; - 切记:不要修改
r->headers_out来影响响应头,那属于输出阶段,与请求审计无关
真正难的不是写逻辑,而是让 header 字节不被 Nginx 自身 parser 提前截断或归一化;很多开发者卡在“明明发了非法头,模块却收不到”,其实是 phase 注册太晚,或者用了动态模块丢了原始链表指针。











