公司域名漏扫安全整改实录

公司域名漏扫安全整改实录

记录时间: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 / vite build.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 走过的弯路

  1. proxy_cookie_flags 管不到本机 sticky cookie:nginx_ingress_route 是 68.154 的 sticky 模块发的,不经 proxy 通道,proxy_cookie_flags 处理不到。route 才是后端返回的。
  2. sticky 模块不支持 samesite 参数:试过 sticky name=xxx httponly secure samesite=none;,nginx -tinvalid argument (samesite=none)。本环境编译的 sticky-module-ng 版本老,不支持 samesite(但支持 httponly/secure)。
  3. nginx 原生 sticky cookie 的 samesite 是 NGINX Plus 专属,开源版没有。
  4. 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
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇