前一阵帮客户做等保复测,安全扫描报告里又出现一条醒目告警:会话cookie中缺少HttpOnly属性。这类问题在IIS环境中出现频率很高——你打开浏览器明明能正常登录,业务也没报错,可扫描器就是盯着响应头里的Set-Cookie不放。今天我就把这个问题的来龙去脉、修复方案和踩坑记录完整梳理一遍,希望你在IIS上遇到同样告警时,能少走一点弯路。
这不是什么高深漏洞,但如果不搞清楚原理,很容易在修复时越搞越乱。很多人第一次看到扫描报告,第一反应是去IIS管理器里找“设置Cookie”的按钮,结果翻遍整个界面也找不到。原因很简单:IIS本身只负责把应用生成的响应头发送出去,它并不知道你的会话Cookie应该带什么属性。真正决定Cookie是否带HttpOnly的,是应用代码和配置文件。理解了这一点,后面所有修复方案就都顺理成章了。
1. 这个告警到底在说什么:先搞懂HttpOnly属性
1.1 会话Cookie是网站的一张“临时通行证”
几乎所有需要登录的网站,都会在用户登录成功后下发一个会话标识,比如ASP.NET环境里的ASP.NET_SessionId,PHP环境里的PHPSESSID。这个标识就是你的“临时通行证”,浏览器每次请求都会把它带在Cookie里,服务器看到这张通行证就知道“哦,这是那个已经登录的用户”,不需要每次重新输密码。
通行证一旦被别人拿走,对方就能冒充你登录。Cookie泄露的常见途径是跨站脚本攻击(XSS)——攻击者往页面里注入一段JavaScript,脚本执行后通过document.cookie读取当前页面的所有Cookie,然后悄悄发到攻击者的服务器上。如果你的会话Cookie没有设置HttpOnly属性,这段脚本就能直接读到通行证内容,用户的会话就被人偷走了。
1.2 HttpOnly属性的防御原理
HttpOnly是Cookie的一个属性,设置之后,浏览器会允许服务器读取这个Cookie,但禁止JavaScript通过document.cookie访问它。换句话说,即使页面被注入了XSS脚本,脚本也无法直接拿到这个Cookie的内容,会话标识被偷的风险就下降了一大截。
这个属性由微软在2002年的IE6 SP1中首次引入,现在所有现代浏览器都已支持。它不需要额外安装组件,也不需要改服务器端的运行时逻辑,纯粹是服务器在Set-Cookie响应头里加上一个标记,浏览器看到后自动执行限制——就这么简单。但正因为实现门槛低,很多老项目、快速开发上线的系统,常常忽略在会话Cookie上加上它。
1.3 为什么IIS上老是被扫描器报这个问题
扫描器判断“会话Cookie缺少HttpOnly属性”的方式并不复杂:它模拟一个普通用户访问站点,观察服务器返回的Set-Cookie响应头,检查其中的会话Cookie是否带HttpOnly标记,没有就报漏洞。
IIS在这里容易“躺枪”,是因为IIS本身只是一个承载应用的平台,它不会替PHP、ASP.NET或静态页面生成统一的会话Cookie策略。很多开发者在使用IIS部署旧版ASP.NET项目时,默认配置并不会给自定义Cookie加上HttpOnly;PHP站点如果没改php.ini和setcookie配置,生成的会话Cookie同样裸奔。甚至有些SPA前端项目,直接用JavaScript在浏览器端创建了会话相关的Cookie——这里有个关键知识点:通过JS设置的Cookie永远无法带上HttpOnly属性,因为HttpOnly本身就是设计来禁止JS读写Cookie的,它必须由服务器在响应头中下发。遇到这种项目,你要么把会话改到后端生成,要么接受扫描器持续报这个告警。
1.4 明确影响范围:哪些Cookie会被盯上
安全扫描报告说的“会话cookie”,在不同站点里指的东西不同,可能是ASP.NET_SessionId,可能是PHPSESSID,也可能是应用自定义的Token、Auth、UserId之类。修复前一定要先搞清楚报告里说的Cookie名称,不要盲目把所有Cookie都加上HttpOnly。有些Cookie是前端用来存用户偏好的,比如记住列表视图模式、记住主题色,这种即使加了HttpOnly也不影响功能,但因为JS读不到,部分前端逻辑反而会报错。会话Cookie和功能性Cookie要区别对待,这是很多初学修复的人最容易忽略的细节。
2. 修复前的环境定位:先给站点做一个“体检”
2.1 用开发者工具确认Cookie名称和来源
先说怎么拿到当前站点实际下发的Cookie。你在浏览器里按F12,切到Network面板,刷新页面,找到主文档请求(doc类型),看Response Headers里的Set-Cookie。如果你有登录操作,登录后的那个请求里通常就会下发会话Cookie。
我习惯再用curl确认一遍,因为浏览器插件有时会干扰。比如:
curl -I -k https://yourdomain.com如果是POST登录接口,则用:
curl -k -i -X POST https://yourdomain.com/login -d "username=test&password=test"在返回的响应头里,看清这些信息:
| 项目 | 需要确认的内容 |
|---|---|
| Cookie名称 | 是ASP.NET_SessionId、PHPSESSID,还是自定义名称 |
| 现有属性 | 是否已有HttpOnly、Secure、SameSite |
| 生成方 | 由后端下发,还是前端JS写入了同名Cookie |
| Cookie作用域 | 是整个根域名,还是某个路径专用 |
这一步做完,你才知道该改配置文件、改代码、改php.ini,还是用URL Rewrite兜底。我见过有人拿着扫描报告直接往Web.config里加了一个<remove name="Set-Cookie" />,结果把登录态直接弄丢了,纯粹因为没搞清楚Cookie是谁下发的。
2.2 确认站点技术栈和IIS版本
不同技术栈,修复入口完全不一样。IIS上常见的有这么几类:
- ASP.NET WebForms / ASP.NET MVC(.NET Framework):走
web.config的httpCookies节点,或代码里的HttpCookie.HttpOnly属性。 - ASP.NET Core:走
Startup.cs里的CookiePolicyOptions,或services.Configure<CookiePolicyOptions>配置。 - PHP(通过FastCGI运行):改
php.ini里的会话配置,或修改setcookie调用参数。 - 纯静态站点:本身不生成会话Cookie,扫描器报的多半是某一个静态资源上的Cookie?实际上静态站一般没有会话,如果报告仍然报了,再看是不是CDN或WAF注入的Cookie。
IIS版本影响的主要是管理界面和模块支持。Windows Server 2012 R2上的IIS 8.5、Server 2016/2019的IIS 10,功能上差别不是很大。如果你要在IIS管理器里操作“身份验证”、“应用程序池”,不同版本菜单略有差异,但Web.config的节点结构是一致的。
2.3 备份是第一优先级,顺便把IIS配置备份做成习惯
修改任何配置前,先在IIS环境里做一个快照级备份。这个方法很多老运维都在用,但新手经常跳过,真出了问题只能干瞪眼。IIS自带了AppCmd工具,可以做备份:
%windir%\system32\inetsrv\appcmd.exe add backup "BeforeHttpOnlyFix"恢复的时候执行:
%windir%\system32\inetsrv\appcmd.exe restore backup "BeforeHttpOnlyFix"这样备份的是IIS整体配置,包括站点、应用池、绑定等。另外,你准备修改的web.config文件也单独复制一份到网站目录之外,防止改错之后无法快速回滚。很多老手还会把C:\Windows\System32\inetsrv\config\applicationHost.config一并备份。等你以后维护多了就会明白,配置备份这件事,看着不起眼,关键时刻能救命。
3. 四种主流修复方案,总有一种适合你的环境
3.1 方案一:ASP.NET项目修改Web.config(首选)
对于传统的ASP.NET WebForms、MVC项目,最简单的办法是在web.config的system.web节点下,添加或修改httpCookies配置:
<configuration> <system.web> <httpCookies httpOnlyCookies="true" requireSSL="true" sameSite="Lax" /> </system.web> </configuration>加完保存,ASP.NET会自动回收应用程序池并应用新配置。这个节点的作用是:把应用通过Response.Cookies下发的所有Cookie,默认都设置为带HttpOnly属性,requireSSL="true"会同时给Cookie加上Secure标记,sameSite="Lax"则能缓解一部分CSRF攻击。
但要注意两个细节:
第一,如果你在代码里显式设置了某个Cookie的HttpOnly为false,配置文件的默认值会被覆盖。老项目里经常出现这种代码:
HttpCookie cookie = new HttpCookie("UserId", userId); cookie.HttpOnly = false; // 这个显式赋值会覆盖web.config的默认设置 Response.Cookies.Add(cookie);这样即使你改了Web.config,这个Cookie依然不带HttpOnly。扫描器如果盯的是这个自定义Cookie,你改完配置也不会通过复测。
第二,httpCookies节点主要影响ASP.NET写入的Cookie,对于第三方组件或者非ASP.NET写入的Cookie,可能管不到。所以方案一改完,一定要按第4节的验证方法再抓一次响应头。
3.2 方案二:代码层显式设置(最稳妥)
如果你的项目还在维护,并且能找到下发会话Cookie的代码,那直接在代码里设置是最清晰、最指向明确的方案。C#里面这么写:
HttpCookie authCookie = new HttpCookie("UserToken", token); authCookie.HttpOnly = true; authCookie.Secure = true; authCookie.SameSite = SameSiteMode.Lax; authCookie.Path = "/"; Response.Cookies.Add(authCookie);如果是通过FormsAuthentication下发的Cookie:
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddMinutes(30), false, userData); string encryptedData = FormsAuthentication.Encrypt(ticket); HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedData); cookie.HttpOnly = true; cookie.Secure = Request.IsSecureConnection; Response.Cookies.Add(cookie);PHP在IIS上同样可以在代码里设置:
setcookie("session_id", $sessionId, [ 'expires' => time() + 3600, 'path' => '/', 'domain' => 'example.com', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]);代码方案的好处是“指哪打哪”,不会误伤其他Cookie;坏处是需要改代码、重新发布。如果只是临时应对扫描,你不太愿意动代码,或找不到源码了,那就看方案四。
3.3 方案三:PHP站点的php.ini配置
如果你的IIS里跑的是PHP应用,最省事的修复是直接改php.ini。在[Session]相关区域调整:
session.cookie_httponly = 1 session.cookie_secure = 1 session.cookie_samesite = Lax这三项改完后重启PHP进程或回收对应应用程序池。它只对PHP会话机制生成的Cookie生效。如果应用里有很多自定义Cookie,那还是得靠setcookie函数层面的httponly参数来补。
另外补充一句:session.cookie_secure = 1只应该在纯HTTPS环境下开启,否则HTTP访问时Cookie无法下发,用户会陷入无限登录循环。如果你还没做全站HTTPS,先不要急着加Secure。
3.4 方案四:IIS URL Rewrite出站规则兜底(不给代码也能修)
这是一个“无代码兜底”方案,适合两种情况:一是找不到源码、没法改代码的遗留系统;二是多个应用统一收敛,想在IIS层面做一层全局修复。
首先需要确保IIS安装了URL Rewrite 2.0模块。没装的话,Web.config里写rewrite节点会直接报“无法识别的配置节”。然后,在站点根目录的web.config中添加出站规则:
<configuration> <system.webServer> <rewrite> <outboundRules> <rule name="Add HttpOnly to Session Cookie" enabled="true"> <match serverVariable="RESPONSE_Set_Cookie" pattern="^(.*SessionId=.*)$" /> <conditions> <add input="{R:1}" pattern="HttpOnly" negate="true" /> </conditions> <action type="Rewrite" value="{R:1}; HttpOnly" /> </rule> </outboundRules> </rewrite> </system.webServer> </configuration>注意这里有坑。Set-Cookie响应头可能在一次请求里包含多个Cookie,每个Cookie之间用逗号分隔,使用URL Rewrite的serverVariable操作必须小心,不要把整个响应头里的所有Cookie全拼到同一行。实际设Rewirte规则时,建议根据你的Cookie名做更精确的匹配,比如^(.*ASP.NET_SessionId=[^;]*)(.*)$。如果站点里同时有多个Cookie需要加HttpOnly,尽量写成多条规则,每条规则只处理一个指定名称的Cookie,避免误伤。
我用这个方案救过不少“只剩部署包、源代码丢了”的情况,确实有效。但它的短板也很明显:规则只是“改头”,如果应用代码显式下发了一个带cookie.HttpOnly = false的Cookie,出站规则是可以覆盖的,因为它改的是IIS响应头级别,比代码晚一步执行。所以它能兜住绝大多数场景。
4. 实操记录:从备份到验证的完整流程
4.1 在Windows功能中确认IIS模块齐全
如果你是在Windows 10/11或Server上用本地环境做复现,先确认IIS已经装上。控制面板 -> “启用或关闭Windows功能”里,展开“Internet Information Services”,至少要勾上“Web管理工具”下的“IIS管理控制台”,以及“万维网服务”下的“常见HTTP功能”、“ASP.NET”或“CGI”(取决于你的应用类型)。
这里顺便提一个很多新人会卡住的点:IIS面板打开后,首页没有“URL重写”图标,那就是没装URL Rewrite模块。去官网下载对应系统版本的rewrite_amd64.msi安装包,装完重启IIS管理器就能看到了。server2019 iis添加进度不动这种问题,多半是打开IIS管理器时在初始化配置,或者权限不足;用管理员身份重新打开一般能解决。
4.2 区分站点类型并应用对应配置
我给客户处理时,一般先按2.1节判断站点类型,再决定用什么方案。举两个真实案例:
案例一是台Windows Server 2012 R2上的老ASP.NET MVC站点,扫描报告点名ASP.NET_SessionId缺少HttpOnly。我在站点的web.config里加上:
<system.web> <httpCookies httpOnlyCookies="true" requireSSL="false" sameSite="Lax" /> </system.web>因为这个站点当时只做了HTTP内网访问,requireSSL没开。保存配置文件后,应用池自动回收,用户重新登录一次,我再抓响应头,ASP.NET_SessionId后面已经带上了HttpOnly标记。
案例二是台Win2019上的PHP站点,报告点名PHPSESSID。我改了php.ini,启用session.cookie_httponly = 1,然后回收应用程序池。这里要特别注意:修改php.ini后很多新手只刷新浏览器,不回收池子,PHP进程还保持着旧的配置,怎么刷新都看不到效果。IIS里PHP是通过FastCGI进程运行的,配置变化后需要回收进程才能生效。我在IIS管理器中找到站点对应的应用程序池,点击“回收”,再看响应头,问题解决。
4.3 验证修改是否生效
修改完成后,验证这一步绝不能省。我习惯分两层验证:
第一层,浏览器开发者工具。F12打开Network,重新请求页面,找到Set-Cookie响应头,确认目标Cookie后面有HttpOnly。如果之前是通过HTTPS访问,同时看下Cookie有没有Secure标记。
第二层,命令行抓头,排除浏览器插件干扰:
curl -I -k https://yourdomain.com如果要看到完整登录流程里的Set-Cookie,用:
curl -k -i -X POST https://yourdomain.com/login -d "username=admin&password=123456"看到类似这行就是成功了:
Set-Cookie: ASP.NET_SessionId=2f3a8b9c...; path=/; HttpOnly; SameSite=Lax之后再用安全扫描器复测,报告中的“会话cookie中缺少HttpOnly属性”就会消失。
这里有个绕不开的坑:如果你的站点前端脚本也在往Cookie里写东西,而你把所有Cookie都设成HttpOnly,前端某些读取逻辑就坏了。比如用户偏好类Cookie,JS在读取时直接返回空字符串,页面功能看起来“时好时坏”。所以修复前必须分清楚哪些Cookie是会话必需的、哪些只是前端功能用的。我一般不会全局把sitecore_analytics、theme这类自定义Cookie都改成HttpOnly,除非业务确认不再通过JS读取它。
4.4 别忘了配合Secure和SameSite一起看
安全扫描报告通常不止查HttpOnly,经常连带着要求Cookie带Secure、SameSite。既然这次改了配置,不如一次性把三个属性都理顺。
- Secure:Cookie只能通过HTTPS传输。如果站点已经全站HTTPS,加上没毛病;如果还有HTTP的URL能访问网站业务,加了需要确认业务链路支持。
- SameSite:主要用来限制第三方请求携带Cookie,缓解CSRF。可选值为Strict、Lax、None。现代浏览器默认已把没有SameSite的Cookie按Lax处理,但扫描报告还是会提示,最好显式声明。
- Domain和Path:最好也显式设置,别让Cookie作用域扩散到不必要的目录。
这三个属性并不冲突,都能在同一行Set-Cookie里共存:
Set-Cookie: SessionId=abc123; path=/; HttpOnly; Secure; SameSite=Lax我在实际项目中习惯按这个标准统一配置,既满足了扫描器要求,也对生产环境更安全。
5. 常见问题与排查技巧实录
5.1 为什么改完Web.config,Cookie还是没带HttpOnly
这是留言区提问率最高的问题。我排查的顺序一般是这样的:
第一,看有没有代码显式覆盖。前面提过,HttpCookie.HttpOnly = false这种代码会把配置文件的默认值盖掉。用文本搜索工具全项目搜一下HttpOnly\s*=\s*false,能搜出一堆历史遗留代码。
第二,看改的是不是站点实际使用的web.config。IIS里站点可以嵌套目录,子目录下的web.config会覆盖根目录的配置。如果你有多个web.config,改错层级也会无效。在IIS管理器中选中对应站点,右键“浏览”,确认站点根目录的真实路径。
第三,看是不是有出站规则和自定义Header模块冲突。有些人之前用customHeaders把Set-Cookie加进所有响应,这实际上是错误的做法——因为Set-Cookie是特殊响应头,不能通过customHeaders追加HttpOnly属性,反而会造成响应头里出现两个Set-Cookie,这会让浏览器糊涂甚至忽略其中一行的属性。正确做法是走URL Rewrite出站规则或应用配置。
第四,回收应用程序池。修改web.config会自动触发回收,但如果你改的是machine.config,这一步不能少。
5.2 IIS报错“执行此操作时出错,文件名: administration.config”这类问题
这个热词搜寻度很高,也常和本次修改动作同时发生。改动Web.config后,如果文件内容格式不对、层级错误、权限不足,IIS管理器在读取配置时就会弹“执行此操作时出错”,并指向C:\Windows\System32\inetsrv\config\administration.config或applicationHost.config这种路径。
我的处理经验:先看事件查看器的“Windows日志-应用程序”里有没有ASP.NET或IIS报错详情,再用命令行工具检查配置语法。跑一下:
%windir%\system32\inetsrv\appcmd.exe list config能正常输出就说明基础配置可读。如果还报错,用备份恢复是最快的。这也就是我在3.3节反复强调备份非常重要的原因。改配置之前,你永远觉得不会出事;出问题时,你一定会后悔没备份。
5.3 一些关联问题:.NET8部署、外网打不开、模块被改
既然本文讲的是IIS,这几个关联热词我也顺带提一下,因为排查HttpOnly时你很可能同时遇到:
- IIS里没有.NET 8的选项:这是很多人在Server 2019上部署新版应用遇到的。IIS管理器里看不到.NET 8,不是你安装问题,而是你需要安装ASP.NET Core Module v2,并且把应用程序池的“.NET CLR版本”设置为“无托管代码”。这和给Cookie加HttpOnly不冲突,都存在同一个环境里。
- 服务器IIS网站外网打不开:如果修完配置,从外网还是无法访问站点,优先检查Windows防火墙是否放行了80/443端口,以及IIS站点绑定是否监听在
*、具体IP还是localhost上。IP变化后,老绑定会悄悄失效。 - IIS模块被篡改写入黑链:这是老生常谈的安全问题了。如果你在排查配置时发现IIS莫名多了很多未知模块或处理程序,要立即检查
applicationHost.config里的<modules>节和<handlers>节。配置安全的底线是:不用的模块移除,未知模块坚决删除。
5.4 常见问题速查表
| 现象 | 最可能原因 | 处理办法 |
|---|---|---|
| 改了web.config不生效 | 代码显式覆盖了HttpOnly=false | 搜索代码,删除或改成true |
| 浏览器里看不到HttpOnly | 看过期缓存或浏览器插件 | 换无痕窗口,用curl重新验证 |
| CustomHeaders设置了Set-Cookie无效 | Set-Cookie不能用customHeaders追加 | 删除customHeaders,用URL Rewrite或代码方案 |
| PHP改了php.ini没变化 | FastCGI进程未回收 | 回收应用程序池或重启进程 |
| 配置后前端功能异常 | Cookie设置了HttpOnly导致JS读不到 | 识别功能性Cookie,单独排除 |
| IIS管理器打不开/报错 | 配置文件权限或语法问题 | 用appcmd检查,恢复备份 |
| 扫描报告仍显示漏洞 | 其他自定义Cookie未处理 | 定位报告里的具体Cookie名,补规则 |
5.5 我个人的一点实操心得
做了这么多IIS安全加固,我个人最大的体会是:配置修复本身不复杂,真正考验人的是“不要扩大修复面”。加HttpOnly是一件防御性操作,但它会影响浏览器端JavaScript对Cookie的可见性。如果你不确认哪些Cookie被前端读取、哪些是后端专用,就一股脑全加上,最终很可能把正常业务改出问题。
另外,很多人改完配置只检查首页,忽略登录接口。实际上会话Cookie往往是在登录成功那一刻才下发的,你不走一遍完整的登录流程,根本看不清修复效果。所以每次验证,我都会先清空浏览器Cookie,再重新走一次登录,再去看Set-Cookie响应头。
最后再分享一个小技巧:如果公司有多个IIS站点,最好把HttpOnly、Secure、SameSite这套配置的校验做成一个固定检查项,不用每次都靠人工抓包。写一个简单的PowerShell循环,模拟请求每个站点的登录页或首页,检查Set-Cookie里是否含HttpOnly,把结果输出成一个表格。定时跑一遍,发现问题早处理,就不用每次等扫描报告送到脸上才手忙脚乱了。