如何修复 PCI DSS 扫描中的 HTTP Security Header Not Detected
结论
这个问题与 IIS 的 CGI 功能没有关系。扫描器将漏洞归到 CGI 类别,只是其内部分类方式。真正需要处理的是:为 HTTPS 站点实际返回的所有响应 添加相应的安全响应头。
Windows Server 2012 R2 通常使用 IIS 8.5,可以在站点根目录的 web.config 中统一配置以下响应头:
X-Frame-OptionsContent-Security-PolicyX-Content-Type-OptionsStrict-Transport-SecurityX-XSS-Protection(主要用于兼容旧扫描规则)
配置完成后,需要从外部访问 https://xxx.abc.com/,检查实际返回的响应头。IIS 管理器中显示了配置项,不代表重定向、错误页、反向代理或应用程序生成的响应一定包含这些响应头。
IIS 8.5 配置示例
将以下内容合并到网站根目录现有 web.config 的 <configuration> 节点中,不要再创建第二个 <system.webServer> 节点。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Frame-Options" />
<remove name="X-XSS-Protection" />
<remove name="X-Content-Type-Options" />
<remove name="Content-Security-Policy" />
<remove name="Strict-Transport-Security" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="X-XSS-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Content-Security-Policy"
value="default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self' https://payment-provider.example;" />
<add name="Strict-Transport-Security"
value="max-age=31536000" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
https://payment-provider.example 只是占位符,必须替换成支付服务商明确要求的真实域名。如果支付流程需要向多个银行、3D Secure ACS 页面或支付网关提交表单,应将这些地址逐一加入 form-action,不能直接使用示例配置上线。
先使用 <remove>,再使用 <add>,可以减少服务器级配置与站点级配置重复,避免最终返回两个同名响应头。
各响应头应该怎样选择
X-Frame-Options
如果网站完全不需要通过 <iframe> 嵌入其他页面,可以使用:
X-Frame-Options: DENY
如果只允许同源页面嵌入,可以使用:
X-Frame-Options: SAMEORIGIN
不要使用已经废弃且浏览器支持不一致的 ALLOW-FROM。如果需要精确允许多个外部来源,应使用 CSP 的 frame-ancestors。
这两个指令的作用方向不同:
frame-ancestors:指定哪些网站可以嵌入当前页面。frame-src:指定当前网站可以加载哪些 iframe。
3D Secure 支付经常涉及 iframe、弹窗、跨域表单或跳转,因此是否适合使用 frame-ancestors 'self',需要结合支付服务商的集成文档判断。
Content-Security-Policy
不能只为了让扫描器检测到响应头,就随意填写 CSP。策略过严可能阻止现有的 JavaScript、CSS、字体、验证码、统计脚本和支付页面正常工作。
上面的策略可以作为一个较严格的起点:
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self' https://payment-provider.example;
如果页面使用了内联脚本、CDN、第三方支付脚本或 iframe,需要根据实际来源添加 script-src、style-src、img-src、connect-src、frame-src 等指令。例如:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com; frame-src 'self' https://secure-payment.example; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self' https://secure-payment.example;
不要为了省事配置宽泛的 *,也应尽量避免长期使用 'unsafe-inline' 和 'unsafe-eval'。
正式启用前,可以先通过以下响应头观察策略的影响:
Content-Security-Policy-Report-Only: ...
需要注意,Content-Security-Policy-Report-Only 不等同于正式的 Content-Security-Policy。如果扫描器明确要求后者,报告模式通常无法消除该扫描项。
X-Content-Type-Options
使用固定值:
X-Content-Type-Options: nosniff
同时要确保 CSS、JavaScript、字体和下载文件返回正确的 Content-Type。启用 nosniff 后,浏览器可能拒绝加载 MIME 类型错误的静态资源。
Strict-Transport-Security
IIS 8.5 可以通过自定义响应头添加 HSTS:
Strict-Transport-Security: max-age=31536000
这个响应头只能通过 HTTPS 返回。浏览器会在 max-age 有效期内强制使用 HTTPS,因此启用前要确认:
- 整个网站都能通过 HTTPS 正常访问;
- TLS 证书有效,续期机制可靠;
- HTTP 请求会跳转到 HTTPS;
- 没有业务入口仍然依赖 HTTP。
首次部署时,可以先设置较短的有效期,例如:
Strict-Transport-Security: max-age=300
确认没有问题后,再提高到一年:
Strict-Transport-Security: max-age=31536000
只有所有子域名都能长期使用 HTTPS 时,才能加入:
Strict-Transport-Security: max-age=31536000; includeSubDomains
不要贸然添加 preload。站点进入浏览器预加载列表后,其影响比普通 HSTS 更难撤销。
X-XSS-Protection
为了兼容旧版扫描器,可以配置:
X-XSS-Protection: 1; mode=block
现代浏览器已经淘汰了这个响应头。它不能替代输出编码、输入校验和 CSP。如果扫描器仍将其列为必检项,可以保留;如果扫描机构允许提交风险说明或申请规则豁免,也可以向扫描服务商确认具体判定标准。
通过 IIS 管理器配置
如果不方便编辑 web.config,也可以在 IIS 管理器中配置:
- 打开
inetmgr。 - 选中具体网站,不要误选服务器级节点。
- 打开 HTTP Response Headers。
- 依次添加上述响应头及对应值。
- 检查是否已有同名响应头,避免重复。
- 重新访问网站并验证实际响应。
修改 web.config 通常会触发应用重新加载,不必重启整台服务器。如果配置没有生效,可以回收对应的应用程序池,但应选择业务允许的时间窗口。
验证实际响应
可以从服务器外部执行:
curl -I https://xxx.abc.com/
预期至少能看到类似结果:
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self' https://payment-provider.example;
Strict-Transport-Security: max-age=31536000
还需要分别检查:
curl -I https://xxx.abc.com/
curl -I https://xxx.abc.com/login
curl -I https://xxx.abc.com/payment
curl -I https://xxx.abc.com/nonexistent-page
检查范围应包括正常页面、登录跳转、支付入口、404/500 错误页,以及返回 301、302、401、403 状态码的响应。PCI 扫描器访问的是 /,因此不能遗漏站点根路径的响应。
如果网站前面还有 CDN、WAF、负载均衡器或反向代理,应以公网最终返回的响应为准。这些中间层可能删除、覆盖或重复添加响应头。
常见失败原因
完成配置后仍然无法通过扫描,常见原因包括:
- 响应头加到了错误的网站或服务器节点。
- 被扫描的主机名绑定到了另一个 IIS 站点。
/返回重定向,但重定向响应没有安全头。- CDN、WAF 或反向代理修改了源站响应。
- ASP.NET 代码或其他 IIS 模块删除、覆盖了响应头。
- 同名响应头出现多次,而且取值冲突。
- HSTS 被错误地加在 HTTP 响应上,HTTPS 响应反而没有。
- CSP 只配置了
Content-Security-Policy-Report-Only。 - 扫描服务仍在使用缓存结果,还没有重新扫描。
最终能否通过扫描,取决于具体 PCI 扫描服务商使用的规则版本。上述配置可以解决“响应中未检测到安全头”的技术问题,但 CSP 白名单仍须根据实际 ASP.NET 和 3D Secure 支付流程调整。上线前,应在测试环境中完整验证支付、回调、验证码和登录流程。
备注:内容仅供参考。
