很多人第一次看到这条漏洞时都会觉得莫名其妙:

网站已经强制 HTTPS,证书也正常,登录请求还是 POST,报告为什么还写着“密码明文传输”?

更麻烦的是,不同测试人员口中的“明文”经常不是同一回事。有人指浏览器开发者工具里能看到密码,有人指登录接口仍能通过 HTTP 访问,还有人发现反向代理解密后,把请求原样转发给了另一台服务器。

这几种情况看起来相似,风险差得很远。报告里只有一句“密码以明文形式提交”,没有请求地址、网络位置和复现过程,基本无法直接判断严重程度。

先弄清楚:密码到底在哪里“明文”

浏览器提交登录表单时,密码在应用层本来就是一段可读字符串。JavaScript 要读取它,浏览器要把它组装进 HTTP 请求,服务端也要在内存中拿到它,才能校验密码。

HTTPS 做的事情,是在 HTTP 数据离开浏览器、进入网络之前,用 TLS 加密整个传输过程。到达 TLS 终点后,数据会被解密,再交给 Web 服务器或应用处理。

因此,在 Chrome 开发者工具的 Request Payload 里看到:

{
  "username": "alice",
  "password": "example-password"
}

不能证明密码正在网络中裸奔。开发者工具展示的是浏览器准备发送的 HTTP 内容,不是网卡上已经加密后的 TLS 数据包。

真正需要查的是下面几种情况。

现象 通常如何判断
开发者工具能看到请求体中的密码 正常现象,单凭这一点不能定漏洞
登录接口可以直接使用 http:// 提交 明确风险,需要立即修复
先用 HTTP 提交,再由服务器跳转到 HTTPS 跳转已经晚了,第一次请求可能泄露
浏览器到 CDN 是 HTTPS,CDN 到源站是 HTTP 要看第二段链路经过哪里,跨公网时风险很高
Nginx 到同机 127.0.0.1 使用 HTTP 需要结合主机隔离、权限和威胁模型评估
请求体、异常信息或访问日志记录了密码 严重问题,泄露面往往比抓包更大
会话 Cookie 没有 SecureHttpOnly 即使登录请求加密,会话仍可能暴露

HTTPS 只保护两个 TLS 端点之间的那段路

部署稍微复杂一点,请求一般不会直接进入业务进程。

浏览器
  │ HTTPS
  ▼
CDN / WAF / Nginx
  │ HTTP 或 HTTPS?
  ▼
后端应用
  │
  ├── 日志系统
  ├── 链路追踪
  ├── 错误上报
  └── 数据库

浏览器地址栏的小锁,只能说明浏览器与当前 TLS 终点之间建立了加密连接。它不会自动保证代理到后端、应用到日志系统、服务到服务之间也使用了 TLS。

TLS 终止代理会在代理处解密 HTTPS,再把请求转发给后端

图:TLS termination proxy,Galgalesh,CC BY-SA 4.0。

这也是反向代理环境最容易出问题的地方。常见配置如下:

location / {
    proxy_pass http://app:8080;
}

用户访问的是 HTTPS,但 Nginx 与 app:8080 之间走的是 HTTP。

这不一定马上等于高危漏洞。如果 Nginx 和应用位于同一台机器,后端只监听 127.0.0.1 或 Unix Socket,普通外部攻击者很难接触这段流量。但如果后端在另一台主机、另一套云网络,或者请求经过了不受控制的公网,风险就完全不同。

Cloudflare 的 Flexible 模式就是很直观的例子:访客到 Cloudflare 使用 HTTPS,Cloudflare 到源站仍是未加密 HTTP。Cloudflare 当前文档建议在条件允许时使用 Full (strict),这样第二段连接也会加密,并验证源站证书。

参考:Cloudflare SSL/TLS 加密模式 · Full (strict)

前端先做一次 SHA-256,并没有解决问题

有些整改建议会让前端在提交前执行:

const value = sha256(password);

然后服务器拿这个哈希值登录。看起来网络里没有出现原始密码,实际上这个哈希值已经成了新的密码。攻击者只要截获它,很多实现里就能直接重放,不需要恢复原文。

客户端哈希还会破坏服务端使用随机盐、工作因子和密码算法升级的能力。普通账号密码登录没有必要自己发明一套“二次加密协议”。先把 TLS、会话和服务端密码存储做好,通常更可靠。

密码进入服务器后,也不能用普通 SHA-256 直接存数据库。OWASP 当前建议优先使用 Argon2id;无法使用时再考虑 scrypt,bcrypt 更适合遗留系统。每个密码都要有独立随机盐,参数也应根据服务器性能调整。

参考:OWASP Password Storage Cheat Sheet

一套能落地的 HTTPS 配置

下面这份 Nginx 配置不追求覆盖所有场景,适合作为检查起点:

server {
    listen 80;
    server_name example.com;

    return 308 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/nginx/tls/fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;

    # 先用较短时间验证,确认所有子域名都支持 HTTPS 后再逐步增加。
    add_header Strict-Transport-Security "max-age=86400" always;

    location / {
        proxy_pass http://127.0.0.1:8080;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

HTTP 跳转和 HSTS 作用不同。跳转需要浏览器先发出一次 HTTP 请求,服务器才能返回 301 或 308;HSTS 生效后,浏览器会在本地把后续 HTTP 访问直接升级为 HTTPS。OWASP 也提醒,includeSubDomainspreload 不适合未经检查就复制粘贴。一旦某个子域名仍依赖 HTTP,贸然开启可能直接导致它无法访问。

参考:OWASP HSTS Cheat Sheet

如果反向代理和业务服务跨主机,建议继续加密代理后的链路,并验证后端证书:

location / {
    proxy_pass https://app.internal.example:8443;

    proxy_ssl_server_name on;
    proxy_ssl_name app.internal.example;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/nginx/ca/internal-ca.pem;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
}

安全要求更高时,可以给内部服务部署 mTLS。这样后端不仅验证代理连接是否加密,还会验证请求方是否持有受信任的客户端证书。

账号密码只在登录时发送一次,会话 Cookie 却会跟随大量请求。如果 Cookie 配置松散,HTTPS 做得再漂亮也保不住登录状态。

推荐从类似下面的配置开始:

Set-Cookie: __Host-session=<随机会话标识>;
  Path=/;
  Secure;
  HttpOnly;
  SameSite=Lax

几个属性分别解决不同问题:

  • Secure:只通过 HTTPS 发送 Cookie。
  • HttpOnly:阻止普通前端 JavaScript 读取会话 Cookie,降低 XSS 窃取风险。
  • SameSite=LaxStrict:减少跨站请求自动携带 Cookie 的场景。
  • __Host-:要求 Cookie 使用 SecurePath=/,并且不能设置 Domain,尽量把作用域限制在当前主机。

MDN 的安全 Cookie 指南也建议对会话标识使用 SecureHttpOnly,并尽量缩小 DomainPath 和有效期。

参考:MDN Secure cookie configuration

别让日志替攻击者保存密码

我见过最离谱的情况,不是登录接口没上 HTTPS,而是开发时为了排查 400 错误,把整个请求体打进了日志:

log.Printf("login request: %+v", req)

之后这些日志又被采集到 Loki、Elasticsearch、Sentry 或第三方 APM。原本只在内存里短暂停留的密码,变成了一份可搜索、可备份、保存几个月的历史记录。

下面这些地方都要检查:

  • Web 服务器是否记录完整查询字符串;
  • 应用是否输出请求体、表单对象或 DTO;
  • 异常上报是否自动附带请求参数;
  • API 网关和 WAF 是否记录敏感字段;
  • 链路追踪 Span 是否带有 Authorization、Cookie 或密码;
  • 测试环境是否把真实账号数据复制进了低权限日志系统。

OWASP 的日志指南明确把认证密码、访问令牌、会话标识、数据库连接串和加密密钥列为通常不应直接记录的数据。

参考:OWASP Logging Cheat Sheet

收到“密码明文传输”报告后怎么复核

先不要急着和测试人员争论,也别立刻安排前端加密。把证据补齐:

  1. 确认完整请求地址:到底是 http:// 还是 https://
  2. 检查表单目标和接口地址:页面是 HTTPS,不代表 form action、Ajax 接口或 WebSocket 也是 HTTPS。
  3. 检查第一次请求:用户输入 http:// 后,是否在提交敏感数据前完成跳转。
  4. 画出完整链路:浏览器、CDN、WAF、负载均衡、Nginx、应用分别在哪里终止 TLS。
  5. 对关键链路抓包:客户端外部链路和代理到后端链路要分开看。
  6. 检查 Cookie:至少确认 SecureHttpOnly 和合理的 SameSite
  7. 搜索日志与追踪系统:用测试账号提交一个唯一标记,确认它没有进入日志。
  8. 检查密码存储:数据库里应保存带盐的慢哈希,不应保存明文或可逆密文。

一份合格的漏洞报告,至少应该写清请求、传输位置、攻击前提和可观察证据。只截一张浏览器开发者工具中的 Request Payload,就给出“高危密码明文传输”,证据是不够的。

反过来,地址栏有锁也不能成为关闭漏洞的理由。代理后的 HTTP 链路、未设置安全属性的 Cookie、请求日志和错误上报,都可能让密码或会话绕开 TLS 的保护。

严重程度怎么定

这类问题没有固定等级,可以按攻击者能否接触敏感数据来判断:

  • 登录接口直接允许 HTTP 提交,攻击者可在用户网络中窃听:通常需要高优先级处理。
  • CDN 到源站跨公网使用 HTTP:风险明显,应该尽快改为端到端 TLS。
  • 跨主机内网使用 HTTP:要看网络隔离、租户边界、旁路监听能力和数据敏感度。
  • 同机回环地址或 Unix Socket:风险相对可控,仍要防止本机高权限进程和错误暴露。
  • 仅仅因为浏览器开发者工具显示请求体:不能据此认定存在传输漏洞。
  • 密码进入日志、APM 或错误上报:这已经超出“传输”问题,往往需要立即清理历史数据、轮换凭据并追查访问记录。

最后

HTTPS 很重要,但它的边界必须说清楚。它保护的是某两个端点之间的数据传输,不负责替你清理日志、保护 Cookie、隔离后端网络,也不会自动把数据库里的密码变成安全哈希。

下次再看到“密码明文传输”,先问一句:在哪一段链路、由谁能够看到、需要什么条件?

这三个问题答不出来,报告无法定级;这三个问题答清楚,整改方案通常也就出来了。


延伸阅读

资料来源