1. 项目概述与核心价值
最近在梳理线上业务的安全基线,发现很多初级开发者甚至部分运维同学,对Nginx的认知还停留在“高性能Web服务器”或“反向代理”的层面,认为安全防护是后端应用防火墙(WAF)或业务代码的事。这其实是一个巨大的误区。Nginx作为流量入口的第一道关卡,其配置的严谨性直接决定了攻击面的大小。一次配置疏忽,比如不当的
location
匹配或缺失的关键指令,就可能将你的应用服务器直接暴露在目录遍历、SQL注入、恶意扫描等常见Web攻击之下。
这个实战教程的核心,就是带你跳出“Nginx只是个转发工具”的思维定式,将其武装成一个具备基础但至关重要的主动防御层。我们不会涉及复杂的WAF模块编译,而是聚焦于Nginx原生配置就能实现的、针对目录遍历、SQL注入、路径穿越、恶意User-Agent等高频攻击的拦截策略。这些配置就像给房子安装最基础的门锁和窗户护栏,成本极低,但能有效阻挡大部分“顺手牵羊”式的自动化攻击。无论你是负责业务部署的运维工程师,还是需要了解基础设施安全的开发人员,掌握这些配置都能让你在构建系统时多一份踏实,在出现安全警报时多一个排查方向。
2. 防御体系设计思路与原则
在动手写配置之前,我们必须先理清防御的思路。Nginx位于网络边界,它的防御逻辑应该遵循“最小权限”和“纵深防御”原则,核心目标是 过滤和阻断明显恶意的请求,减轻后端应用的压力 ,而不是替代应用层的业务安全校验。
2.1 防御策略分层
我的策略通常分为三层:
- 请求结构层过滤 :针对不符合HTTP协议规范或明显畸形的请求进行拦截。例如,异常的请求方法、过长的URI、包含特殊字符的路径等。
-
语义规则层拦截
:针对具有明确攻击特征的请求内容进行匹配和阻断。这是本次实战的重点,包括检测URL中的敏感路径(
../)、SQL关键词(union select)、系统命令(; cat /etc/passwd)等。 -
访问行为层限制
:针对高频、恶意的访问行为进行限制,如对特定URI的暴力破解、扫描器爬虫的访问等。这部分通常需要结合
limit_req和limit_conn模块。
一个关键认知是:Nginx的规则匹配是 大小写敏感 且 基于正则表达式 的。攻击者经常会使用大小写变换、编码、注释符分割等技巧来绕过简单的字符串匹配。因此,我们的规则必须考虑这些绕过手法,使用更严谨的正则表达式。
2.2 核心配置模块与位置
主要用到的Nginx模块是
ngx_http_core_module
和
ngx_http_rewrite_module
。防御配置放置的位置至关重要:
-
http块内 :定义全局的映射表(map)、共享的限流规则等。 -
server块内 :针对特定域名的通用防护规则,如屏蔽非法域名访问、定义错误页面。 -
location块内 :最精细的防护,针对具体的应用路径设置规则。 绝大多数攻击检测规则应放在location / {}这个处理所有请求的块中,或者放在业务location之前 ,确保请求首先经过安全过滤。
注意 :规则的顺序就是匹配的顺序。应将最严格、最可能命中的阻断规则放在前面,将一些记录日志的规则放在后面,以提高效率。
3. 核心攻击拦截配置实战详解
下面,我们进入核心环节,逐项拆解如何配置。我会给出配置片段,并解释每一行背后的意图和可能遇到的坑。
3.1 防御目录遍历与路径穿越
目录遍历(Directory Traversal)攻击的核心是利用
../
或其变体跳出Web根目录,访问系统文件。防御的关键是检测请求URI中是否包含这些序列。
server {
listen 80;
server_name yourdomain.com;
# 关键:定义Web根目录,这是所有相对路径的基准
root /var/www/html;
location / {
# 防御目录遍历:拦截包含 ../、..\、./、.\\ 等路径穿越序列的请求
if ($request_uri ~* "\.\.(/|%2f|%5c)") {
return 403;
}
if ($request_uri ~* "\.(/|%2f|%5c)") {
# 注意:单纯一个点“.”可能是正常请求,这里示例拦截“./”,实际需谨慎
# 更常见的做法是结合其他特征,这里仅作演示
return 403;
}
# 防御编码绕过:拦截URL编码的穿越序列,如 %2e%2e%2f (../), %2e%2e/ (../)
# 也防御反斜杠版本(Windows路径)及其编码 %2e%2e%5c
if ($request_uri ~* "%2e%2e") {
return 403;
}
# 可选:防御绝对路径访问(某些配置错误下可能生效)
if ($request_uri ~* "^/(etc|bin|usr|var|root|windows|winnt)") {
return 403;
}
# 你的正常代理规则或静态文件服务规则放在下面
proxy_pass http://backend_server;
}
# 自定义403错误页面,避免暴露服务器信息
error_page 403 /403.html;
location = /403.html {
internal;
root /usr/share/nginx/html;
}
}
实操心得 :
-
$request_uri变量包含了原始的、未经解码的请求URI(包括查询参数),而$uri是解码并规范化后的。 防御时一定要用$request_uri,因为攻击载荷可能在查询参数里,且编码可能不同。 -
正则表达式
~*表示不区分大小写匹配。\.\.匹配两个点,(/|%2f|%5c)匹配正斜杠、URL编码的正斜杠或反斜杠。这个组合能覆盖../、..\、%2e%2e%2f等多种形式。 -
对“.”的拦截要非常小心,因为很多正常的CSS、JS文件引用会使用
./。除非你有非常明确的理由,否则不建议单独拦截“.”,更应关注“..”的组合。 -
return 403;直接返回403状态码,中断请求处理。比deny all更灵活,可以自定义错误页面。
3.2 防御SQL注入探测
SQL注入攻击的探测特征通常体现在查询字符串(
$args
或
$query_string
)中。我们的目标是拦截包含常见SQL关键字和操作符的恶意请求。
http {
# 使用map创建一个变量,用于标记请求是否可疑。map块放在http块内,只计算一次,效率高。
map $args $sql_injection {
default 0;
# 匹配SQL关键词和操作符,忽略大小写。\b表示单词边界,提高准确性。
~* (\bunion\b.*\bselect|\bselect.*\bfrom|\binsert\b.*\binto|\bupdate\b.*\bset|\bdelete\b.*\bfrom|\bdrop\b|\btruncate\b|\balter\b|\bexec\b|\bexecute\b|\bxp_cmdshell\b) 1;
~* (\bor\b|\band\b)\s*['\d] 1; # 匹配 'or 1=1', 'and 1=1' 等变体
~* (\bwaitfor\b.*\bdelay\b|\bsleep\s*\(|\bbenchmark\s*\() 1; # 匹配时间盲注特征
~* (--|\#|\/\*.*\*\/) 1; # 匹配SQL注释符
~* (\bconcat\s*\(|\bgroup_concat\s*\() 1; # 匹配常见拼接函数
}
# 另一个map,检查URI路径部分是否包含可疑内容(有时攻击载荷在路径中)
map $request_uri $uri_sql_injection {
default 0;
~* (\bunion\b.*\bselect|\bselect.*\bfrom) 1;
}
}
server {
listen 80;
server_name yourdomain.com;
location / {
# 检查map变量,如果值为1,则阻断请求
if ($sql_injection = 1) {
# 可以记录到特殊日志,便于分析
access_log /var/log/nginx/sql_block.log main;
return 403;
}
if ($uri_sql_injection = 1) {
access_log /var/log/nginx/sql_block.log main;
return 403;
}
# 额外防御:拦截常见的注入测试参数名
if ($query_string ~* "(id|user|name|pass|password|account|login|query|search|q)=[^&]*[';]") {
return 403;
}
proxy_pass http://backend_server;
}
}
注意事项与高级绕过 :
-
误报风险
:这是最大的挑战。例如,网站搜索功能可能允许用户输入“union”、“select”等词。因此,
必须将规则细化到具体的
location。对于已知的安全接口(如/api/search),可能需要放宽或禁用某些规则。可以使用location ~ ^/api/search$ { ... }来定义例外。 -
编码绕过
:攻击者会使用URL编码(
%27代表单引号')、双重编码、HTML编码等。我们的正则匹配的是Nginx接收到的变量,而$args和$request_uri在匹配前已经进行了一部分解码。但为了更全面,可以在map规则中加入编码形式的匹配,例如%27(单引号)、%2527(双重编码的单引号)。但要注意,过度匹配会增加误报和性能开销。 -
性能考量
:复杂的正则表达式会影响性能。
map指令在http块中定义,只在请求处理初期计算一次,然后将结果(0或1)存储在变量中,在多个location中复用,效率很高。应优先使用map。 -
记录日志
:拦截时记录到独立日志(
sql_block.log),便于后续安全审计和规则调优。不要和普通访问日志混在一起。
3.3 防御其他常见Web攻击
除了上述两种,还有大量自动化工具发起的常见探测攻击。
server {
location / {
# 1. 防御敏感文件访问 (如 .git, .svn, .env, 备份文件)
if ($request_uri ~* "\.(git|svn|htaccess|htpasswd|env|bak|old|swp|sql|tar\.gz|zip)$") {
return 404; # 返回404比403更好,不暗示文件存在
}
if ($request_uri ~* "/\.(git|svn)") {
return 404;
}
# 2. 防御命令注入特征 (分号、管道符、反引号等)
if ($query_string ~* "[;|&`<>]") {
# 注意:&是正常的查询参数分隔符,此规则可能误报,需结合上下文或调整正则
# 更安全的做法是匹配命令注入模式,如 `; ls`, `| cat`
if ($query_string ~* ";(ls|cat|id|whoami|pwd)\s") {
return 403;
}
}
# 3. 防御恶意User-Agent (常见扫描器、漏洞利用工具)
if ($http_user_agent ~* "(nmap|sqlmap|nikto|acunetix|nessus|w3af|dirb|hydra|metasploit)") {
# 可以直接返回444,Nginx会立即关闭连接,不发送任何响应头
return 444;
# 或者记录并返回假数据
# access_log /var/log/nginx/scanner.log;
# root /dev/null;
# return 200 "Not Found";
}
# 拦截空或非常规的User-Agent
if ($http_user_agent = "-" ) {
return 444;
}
# 4. 限制HTTP方法,只允许GET, POST, HEAD等
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$) {
return 405; # Method Not Allowed
}
# 5. 限制请求体大小,防止缓冲区溢出攻击
client_max_body_size 10M;
# 6. 隐藏Nginx版本号(在http或server块中配置)
server_tokens off;
proxy_pass http://backend_server;
}
}
配置详解与取舍 :
-
敏感文件拦截
:返回
404而非403是一种“安全模糊”策略,让攻击者无法区分是文件不存在还是被拦截。 -
命令注入防御
:直接匹配
[;|&<>]误报率极高,因为&是正常参数分隔符,<和>可能在搜索功能中出现。**最佳实践是结合具体上下文**,例如只在对cmd、exec`这类高危参数进行匹配,或者像示例中匹配具体的命令模式。在实际生产中,这部分逻辑更适合放在专业的WAF或应用层。 -
User-Agent拦截
:返回
444(Nginx自定义的非标准状态码,直接关闭连接)对扫描器非常有效,能节省服务器资源。但要注意,一些合法的爬虫(如某些云监控)可能使用非常规UA,需要加白名单。白名单可以通过map实现:map $http_user_agent $is_allowed_ua { ... }。 -
HTTP方法限制
:对于纯API服务器或静态网站,严格限制方法很有效。但对于需要处理
PUT、DELETE等方法的RESTful API,则需要调整。
4. 高级防护:组合使用Map与限流
单一的匹配拦截是基础,结合
map
和限流模块能构建更智能的防御。
4.1 使用Map管理黑名单与评分
我们可以创建一个“威胁评分”系统,对请求的不同可疑特征进行加分,超过阈值则阻断。
http {
# 初始化一个请求的威胁分数
map $remote_addr $request_threat_score {
default 0;
}
# 定义多个map来检查不同特征并赋值分数
map $args $sql_score {
default 0;
~* (\bunion\b.*\bselect) 10;
~* (--|\#) 3;
}
map $request_uri $traversal_score {
default 0;
~* "\.\./" 10;
}
map $http_user_agent $ua_score {
default 0;
~* (sqlmap|nmap) 15;
~* "-" 5;
}
# 在server或location中,通过set指令累加分数(这是一个简化示例,实际需要更复杂的逻辑,可能借助Lua模块)
# Nginx原生配置难以实现动态累加,此处仅为思路展示。
# 更实用的做法是:定义多个map,每个map对应一个动作(0通过,1阻断),然后用多个if判断。
}
虽然原生Nginx难以实现复杂的累加逻辑,但我们可以定义多个独立的
map
,每个对应一个阻断条件,然后用多个
if
语句判断,逻辑更清晰。
4.2 配置请求频率限制
这是防御暴力破解和CC攻击的利器。
http {
# 定义一个限制请求速率的共享内存区,名为`one`,大小10m,每秒最多10个请求
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
# 定义一个限制连接数的共享内存区
limit_conn_zone $binary_remote_addr zone=addr:10m;
}
server {
location /login {
# 应用限流:每秒最多处理10个请求,突发队列不超过5个
limit_req zone=one burst=5 nodelay;
# 限制同一IP同时只能有1个连接
limit_conn addr 1;
# 超过限制时返回429状态码
limit_req_status 429;
limit_conn_status 429;
proxy_pass http://backend_auth_server;
}
location /api/ {
# 对API接口也可以实施较宽松的限流
limit_req zone=one burst=20;
proxy_pass http://backend_api_server;
}
}
参数解读 :
-
limit_req_zone ... zone=one:10m rate=10r/s;:在http块定义。$binary_remote_addr以二进制格式存储客户端IP,更省空间。10m的内存可以存储大量IP的状态。rate=10r/s表示每秒10个请求。 -
limit_req zone=one burst=5 nodelay;:在location中应用。burst=5允许超过速率后排队5个请求。nodelay表示对于排队中的请求也立即处理,而不是延迟处理,这适用于需要快速响应的场景(如登录),但会瞬间消耗更多后端资源。不加nodelay则会平滑处理。 -
limit_conn_zone和limit_conn:用于限制同一IP的并发连接数,对防止慢速攻击或连接耗尽有效。
5. 配置调试、测试与监控
配置写好了,如何验证其有效性并监控拦截效果?
5.1 配置语法检查与重载
# 1. 每次修改配置文件后,务必先检查语法
nginx -t
# 2. 如果语法正确,平滑重载配置(不影响在线请求)
nginx -s reload
# 如果-t报错,根据错误信息定位行号修改。常见错误:括号不匹配、分号缺失、未知指令。
5.2 模拟攻击测试
使用
curl
命令模拟恶意请求,测试拦截规则。
# 测试目录遍历
curl -I "http://yourdomain.com/../../etc/passwd"
curl -I "http://yourdomain.com/%2e%2e/%2e%2e/etc/passwd"
# 测试SQL注入
curl -I "http://yourdomain.com/?id=1' OR '1'='1"
curl -I "http://yourdomain.com/?id=1 UNION SELECT null,user(),null"
# 测试敏感文件访问
curl -I "http://yourdomain.com/.git/config"
curl -I "http://yourdomain.com/backup.zip"
# 测试恶意User-Agent
curl -I -A "sqlmap/1.7.0" "http://yourdomain.com/"
curl -I -A "-" "http://yourdomain.com/"
# 观察返回状态码,应为403、404、444或429(限流时)。
5.3 日志分析与监控
配置独立的拦截日志,便于分析和告警。
http {
log_format security_block '$remote_addr - [$time_local] "$request" '
'$status "$http_user_agent" "$http_referer" '
'Blocked_Reason: $request_uri';
# 在拦截的if块中使用
# if ($sql_injection = 1) {
# access_log /var/log/nginx/security_block.log security_block;
# return 403;
# }
}
定期检查
/var/log/nginx/security_block.log
,可以看到被拦截的请求详情和原因。可以将此日志接入ELK(Elasticsearch, Logstash, Kibana)或类似监控系统,设置告警规则,例如:同一IP在短时间内触发多次拦截,则发送告警。
5.4 常见问题排查实录
问题1:规则导致正常业务请求被拦截(误报)。
-
排查
:查看Nginx错误日志(
error.log)和上述安全拦截日志,找到被误拦的请求URL和特征。 -
解决
:
-
细化规则
:将过于宽泛的正则表达式收紧。例如,匹配
union select时,确保前后有单词边界\b。 -
调整作用域
:将通用规则从
location /移到更具体的、不涉及敏感功能的location块。或者为特定的安全接口(如搜索)创建单独的location并禁用某些规则。 -
使用白名单
:对于已知的安全参数或路径,使用
if条件排除。例如:if ($request_uri !~ "^/api/secure-search") { ... 应用规则 ... }。
-
细化规则
:将过于宽泛的正则表达式收紧。例如,匹配
问题2:攻击请求绕过了规则(漏报)。
-
排查
:检查攻击载荷是否使用了双重编码、大小写混合、注释符分割等技巧。对比
$request_uri和$uri的值。 -
解决
:
-
完善正则
:在
map规则中增加对编码变体的匹配。例如,同时匹配union、Union、UNION以及%55%4e%49%4f%4e(URL编码)。 -
多层检测
:结合
$args、$request_uri、$http_user_agent等多个变量进行综合判断,提高单个特征的门槛(如分数累加思路)。 - 考虑专业WAF :对于复杂的、变形的攻击,原生Nginx正则匹配可能力不从心。此时应考虑集成ModSecurity(开源WAF)或使用云WAF服务。
-
完善正则
:在
问题3:配置重载后,部分规则不生效。
-
排查
:确认配置文件语法检查通过。检查规则所在的
server块或location块是否被正确匹配。使用curl测试时,确保域名和端口正确。 -
解决
:Nginx的
if指令在location中有一些限制(它是“重写”模块的一部分,在某些上下文中行为可能不符合预期)。如果遇到奇怪的问题,尝试将复杂的if判断逻辑移到map指令中定义,map的匹配更高效且行为更可预测。
问题4:限流配置未生效。
-
排查
:检查
limit_req_zone和limit_conn_zone定义的内存区名称、大小是否与location中引用的zone名称一致。检查客户端IP$binary_remote_addr是否能够正确获取(如果前端有代理,可能需要使用$http_x_forwarded_for,但要小心伪造)。 -
解决
:确保
limit_req和limit_conn指令放在需要限制的location块内。对于经过多层代理的情况,需要配置realip模块来获取真实客户端IP用于限流。
经过以上从原理到实战,从配置到排查的完整梳理,你应该已经能够为你的Nginx搭建起一道坚实的基础防线。记住,安全是一个持续的过程,配置上线后,持续的日志监控和规则调优与最初的部署同样重要。这套配置不能防御所有攻击,但它能像一张滤网,帮你过滤掉绝大部分噪音和低层次威胁,让你和你的后端应用能更专注于业务逻辑本身。

354

被折叠的 条评论
为什么被折叠?



