先把同一地址在“未登录的桌面浏览器”“已登录的桌面浏览器”“移动设备浏览器”三种条件下分别抓取并保存为独立文件,再逐项对照差异。不要用肉眼切换窗口比较,因为差异可能来自缓存、Cookie、User-Agent 或服务端分流,肉眼很难区分。对照的目标是判断差异是“同一页面的渲染变化”还是“服务端返回了不同文档”,两者的处理路径完全不同。
同一 URL 返回不同内容,最常见的原因是请求头不同。服务端可能根据 Cookie、User-Agent、Accept-Language 甚至 IP 段返回不同模板。因此对照前要先固定可变量,只保留一个变量在变化。
具体动作:用命令行工具分别抓取,并显式指定请求头,把结果存成文件。例如:
curl -s -D headers-anon.txt -o body-anon.html "https://example.com/page"
curl -s -D headers-login.txt -o body-login.html -H "Cookie: session=..." "https://example.com/page"
curl -s -D headers-mobile.txt -o body-mobile.html -H "User-Agent: Mozilla/5.0 (iPhone...)" "https://example.com/page"
保存响应头(-D)和响应体(-o)同样重要。响应头里的 Vary、Cache-Control、Set-Cookie 和状态码,往往直接解释了为什么内容不同。做完这一步,你手里就有了三份可复查的原始证据,而不是印象。
拿到文件后,先看响应头再看正文。很多“内容不同”其实是缓存层或重定向造成的,与安全扫描本身无关。
Vary 头。如果它包含 User-Agent 或 Cookie,说明服务端明确按这些维度返回不同内容,这是设计行为,不是异常。Cache-Control 和 Age。如果匿名请求命中了 CDN 缓存,你拿到的可能是几小时前的版本。Set-Cookie。登录态可能被服务端重新下发了会话标识,导致后续请求行为改变。假设一个场景:匿名请求返回 200 且 Vary: User-Agent,登录请求返回 200 且 Vary: Cookie,移动请求返回 200 但响应体只有一段 JavaScript 壳。此时可以初步判断:前两者是服务端按条件渲染,移动端可能是客户端渲染。这个判断会决定下一步是去比对渲染后的 DOM,还是继续比对服务端 HTML。
如果响应头没有解释差异,就进入正文对照。用 diff 或任意文本比较工具,但要注意:直接 diff 两份 HTML 通常噪音很大,因为时间戳、随机 token、CSRF 字段每次请求都不同。
更可操作的做法是先提取关键结构再比较:
<title>、<h1> 和主要 <a href> 列表,比较这些稳定字段。这里有一个容易遗漏的条件:命令行抓取拿到的是初始 HTML,不含 JavaScript 执行后的结果。如果页面是客户端渲染,curl 的三份结果可能看起来一样,但你用浏览器看到的却不同。这时需要改用能执行脚本的抓取方式,或在浏览器开发者工具里分别导出渲染后的 DOM 再对照。
对照完成后,把差异归入以下三类之一,因为每类对应的处理动作不同:
Vary,正文结构不同。动作是确认分流规则是否符合预期,检查登录态是否暴露了本不该给匿名用户的内容。归类的依据是响应头和初始正文,而不是你最终在屏幕上看到的样子。这个顺序很重要:如果先看渲染结果,很容易把缓存问题误判为权限问题。
把上面的步骤压缩成一份可重复的清单,每次遇到同一地址返回不同内容时按顺序执行:
Vary,排除重定向和缓存。<title>、<h1>、主要链接,判断是否同一文档。这套流程的价值在于:它把“为什么不一样”拆成了可以逐个排除的条件,而不是停留在反复刷新页面。每次对照都留下文件,后续复查或交接时可以直接引用,不必重新猜测当时的设备状态和登录状态。