公司域名漏扫安全整改实录
记录时间:2026-07-30(前一日 + 当日)
环境:生产 nginx 反向代理(68.154),转发路径客户端 → WAF(openresty) → nginx → K8s ingress;配置目录/AppHome/nginx2021/conf/;域名example.cn,443 与 8443 双 server 块
一、背景
漏扫针对 example.cn 报了一批问题,跨两天发现:
- 前一日:sourcemap 资源可访问(
/cas/webjars/zxcvbn/4.3.0/zxcvbn.js.map) - 当日:
/cusplus、/airedo、/gzszdz三个路径的安全响应头缺失、CORS 不安全配置、会话 cookie 缺 Secure/HttpOnly/SameSite
本篇记录这一轮整改的全部处置与走过的弯路。
二、整改清单
| 问题 | 受影响路径 | 处置手段 |
|---|---|---|
| sourcemap 暴露源码映射 | /cas/*.map |
正则 location 返回 404 |
| 安全响应头缺失 | /cusplus /airedo /gzszdz | 公共 snippet 补齐 11 个头 |
| CORS 不安全配置 | /cusplus /airedo | proxy_hide_header + Origin 白名单反射 |
| 会话 cookie 缺 Secure/HttpOnly/SameSite | /cusplus /airedo | 去掉 68.154 冗余 sticky,后端 route cookie 接管 |
三、sourcemap 拦截(前一日)
漏扫发现 /cas/webjars/zxcvbn/4.3.0/zxcvbn.js.map 可访问。.map 是 sourcemap,记录打包后代码到源码的映射,生产环境暴露等于把前端源码结构送出去。
# 拦截 /cas 路径下的 sourcemap(.map)资源
# 正则 location 优先级高于 /cas 前缀 location,会先命中并返回 404
location ~* ^/cas/.*\.map$ {
return 404;
}
443 和 8443 两个 server 块各放一份。要点:
- 必须用正则 location(
~*):它优先级高于普通前缀/cas,能在请求被代理到后端前命中。~*不区分大小写。 - 返回 404 不返回 403:404 表示”不存在”,不暴露”有但禁止”。
- 只拦 /cas,不要全局拦:各业务路径归属不同后端,全局
~* \.map$可能误伤某些业务合法对外提供的 .map。漏扫点名哪个路径,就给哪个路径加。 - 治本在构建侧:webpack
devtool: false/ vitebuild.sourcemap: false,nginx 是兜底。
四、安全响应头补齐(当日)
漏扫报告 /cusplus /airedo /gzszdz 缺失一批安全响应头(CSP、Referrer-Policy、Permissions-Policy、X-Permitted-Cross-Domain-Policies、X-Download-Options、CORP/COOP/COEP 等)。抽成公共 snippet snippet/security-headers.conf:
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: ws: wss:; frame-ancestors 'self'" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Download-Options "noopen" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header Cross-Origin-Resource-Policy "same-site" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
add_header Cross-Origin-Embedder-Policy "unsafe-none" always;
location 内 include snippet/security-headers.conf; 引入。要点:
- nginx add_header 继承陷阱:location 内一旦出现任意
add_header,就不再继承 server 块的 add_header。所以 snippet 把 HSTS / X-Content-Type-Options / X-XSS-Protection 一并声明,否则该 location 会丢这三个头。 - CSP 用宽松策略:
default-src 'self'曾直接打挂前端 SPA(历史教训),故放开 inline/eval/data/blob/https/ws,只保留 frame-ancestors 防点击劫持,先满足”有 CSP 头”的合规要求。 - COEP 用 unsafe-none,不用 require-corp(否则跨域资源全部加载失败)。
- 不加 Clear-Site-Data:它会清浏览器 cookie/cache/storage,加在业务 location 上会让用户登出、白屏,只在”退出登录”接口用。
- 不加 Access-Control-Allow-Origin
*:同源 SPA 不需要,加*是安全倒退。CORS 走白名单(见第五节)。
五、CORS 不安全配置修复(当日)
漏扫报告 /cusplus /airedo 的 CORS 不安全配置。nginx 层原本无任何 CORS 配置,不安全的 ACAO 来自后端 ingress,被透传。
5.1 Origin 白名单(nginx.conf http 块)
map $http_origin $cors_allow_origin {
default "";
"~*^https://example\.cn$" "$http_origin";
"~*^https://([a-z0-9-]+\.)?trusted\.example\.cn$" "$http_origin";
}
白名单:本站 + trusted.example.cn 及其子域。非白名单 Origin → 变量为空 → 不输出 ACAO → 浏览器拒绝跨域。
5.2 CORS 片段 snippet/cors-cross-site.conf
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Expose-Headers;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Credentials "true" always;
proxy_cookie_flags ~.* secure httponly samesite=None;
要点:
proxy_hide_header先剥后端的 CORS 头,跨域放行权完全由 nginx 白名单决定,即使后端返回*也被剥掉。$cors_allow_origin为空时,add_header不输出 ACAO(nginx 行为:值为空不加该头)。Allow-Credentials: true配合白名单反射(非*),跨站带 cookie 安全。proxy_cookie_flags ~.* ... samesite=None只处理后端返回的 cookie(如 route),管不到本机 sticky cookie(见第六节)。
六、会话 cookie 安全属性(这节最折腾)
漏扫报告 /cusplus /airedo 的会话 cookie 缺 Secure/HttpOnly/SameSite。
6.1 双重 cookie 的真相
curl 发现两个 Set-Cookie:
nginx_ingress_route:68.154 本机 sticky 模块生成。route:后端 K8s ingress 的 affinity cookie(session-cookie-name: route,后端配了 Secure/HttpOnly/SameSite=None)。
6.2 走过的弯路
proxy_cookie_flags管不到本机 sticky cookie:nginx_ingress_route是 68.154 的 sticky 模块发的,不经 proxy 通道,proxy_cookie_flags 处理不到。route才是后端返回的。- sticky 模块不支持 samesite 参数:试过
sticky name=xxx httponly secure samesite=none;,nginx -t报invalid argument (samesite=none)。本环境编译的 sticky-module-ng 版本老,不支持 samesite(但支持 httponly/secure)。 - nginx 原生
sticky cookie的 samesite 是 NGINX Plus 专属,开源版没有。 - lua 走不通:68.154 是 nginx(
server: openresty是前面 WAF 的,不是 68.154),没编译 ngx_lua。
6.3 最终方案:去掉冗余 sticky
发现双重 sticky:68.154 粘到 ingress controller 节点(nginx_ingress_route)+ 后端 ingress 粘到 pod(route)。后端 route cookie 已有完整属性,68.154 那层 sticky 是冗余,还是 SameSite 元凶。
upstream ingress {
server 10.202.17.1:80 max_fails=3 fail_timeout=10s;
server 10.202.17.2:80 max_fails=3 fail_timeout=10s;
server 10.202.17.3:80 max_fails=3 fail_timeout=10s;
# sticky name=nginx_ingress_route httponly secure; ← 去掉
keepalive 128;
}
效果:nginx_ingress_route 不再下发,只剩后端 route(三属性齐全)。pod 级粘性由 route cookie 保证(ingress controller 无状态,任一节点收到带 route 的请求都能转发到同一 pod)。
要点:
- 去掉前确认 ingress controller 无状态(K8s nginx-ingress-controller 通常无状态)。
- 直连应用的 upstream(如 xcstat/xcsso,非 ingress controller)不能去 sticky,它们没有后端 affinity 接力,去掉会丢会话。
七、snippet 复用体系
整改中抽出公共片段(放 conf/snippet/):
| snippet | 职责 |
|---|---|
security-headers.conf |
11 个通用安全响应头 |
cors-cross-site.conf |
CORS 白名单反射 + Allow-Credentials + 后端 cookie 属性 |
location 按需 include:
- 仅同源(如 /gzszdz):
include snippet/security-headers.conf; - 跨站带 cookie(如 /cusplus /airedo):
include snippet/security-headers.conf;+include snippet/cors-cross-site.conf;
部署关键:
snippet/目录必须同步到/AppHome/nginx2021/conf/snippet/,否则nginx -t报 include 找不到文件。cp -r 68.154/conf/* /AppHome/nginx2021/conf/会一并复制;手动单文件 scp 会漏。
八、验证
nginx -t && nginx -s reload
# 1. sourcemap 拦截
curl -I https://example.cn/cas/webjars/zxcvbn/4.3.0/zxcvbn.js.map
# 预期:404
# 2. 安全头 + cookie + CORS
curl -I https://example.cn/cusplus 2>&1 \
| grep -iE 'set-cookie|access-control|content-security|referrer|permissions|cross-origin|x-permitted|x-download'
# 预期:只剩 route cookie(Secure;HttpOnly;SameSite=None),11 个安全头齐全,Allow-Credentials: true
# 3. 白名单外 Origin 不反射 ACAO
curl -sv -H "Origin: https://evil.example.com" https://example.cn/cusplus 2>&1 | grep -i access-control-allow-origin
# 预期:无输出(浏览器将拒绝跨域)
九、注意事项
- 会话行为变更:去掉 ingress sticky 后,pod 级粘性靠后端 route cookie。curl 只能验证 cookie 下发,实际会话保持需用浏览器登录走一遍业务流程确认。
- 其余 28 个走 ingress 的 location:漏扫若继续点名,加
include security-headers.conf(+ cors-cross-site.conf 如需跨域)即可,sticky 不用动(共用 upstream ingress)。 - kj-ingress / xcstat / xcsso 的 sticky 未动:kj-ingress 待确认后端是否同套 affinity;xcstat/xcsso 是直连应用不能去。
- CSP/COEP/COOP/CORP 是宽松通用值,未核对各前端实际资源,若 reload 后资源加载或弹窗异常,对应值放宽或移除。
- 不要为过漏扫加
Access-Control-Allow-Origin: *或Clear-Site-Data:前者是安全倒退,后者会清登录态。
十、参考资料
- nginx location 匹配:https://nginx.org/en/docs/http/ngx_http_core_module.html#location
- nginx proxy_cookie_flags:https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cookie_flags
- nginx-sticky-module-ng:https://github.com/Refinitiv/nginx-sticky-module-ng