把管理界面迁到 admin.example.com 是真正的改进:它让管理 Cookie 远离公开页面,给两个应用各自独立的缓存,也能把更严格的网络策略指向单一主机名。但它不是 CSRF 防护,也不会让公开站点的代码在已登录管理员面前变得安全。这种分离改变的是浏览器眼中的边界,而不是服务器检查的内容,所以在宣布完工之前,请完成这四项检查。

把源和站点当成两个不同的词

源是协议、主机和端口,所以 https://www.example.com 与 https://admin.example.com 是两个不同的源。SameSite 意义上的站点是协议加可注册域,所以这两个主机名通常属于同一个站点。差别就是全部陷阱:SameSite 属性在兄弟子域之间帮不上忙,因为浏览器根本不把该请求当作跨站。把管理界面指向真正不同的可注册域,例如 admin-example.net,请求才真正变成跨站,SameSite=Lax 或 Strict 也才在那里开始做可靠的工作。

请在浏览器里实测,而不是空想。

加载公开站点,打开开发者工具,看看浏览器把哪些 Cookie 附加到发往管理主机的请求上。如果 Cookie 没有 Domain 属性,它只属于该主机,会留在自己的主机名上;如果带有 Domain=example.com,它会发往每个兄弟主机,新子域对这个 Cookie 毫无帮助。

把能读响应和能发请求分开

同源策略限制的是跨源读取响应。它不阻止请求被发出,也不阻止表单携带浏览器已有的凭据。简单请求,例如表单内容类型或普通 GET,不经预检就会送达;之后 CORS 才决定调用脚本能否读取响应。CSRF 完全发生在发送一侧。

因此服务器必须对改变状态的操作做显式检查。与会话绑定的同步令牌,或服务端比对的双提交 Cookie,都能覆盖这种情况;Origin 或 Referer 比对值得作为第二道门,但不能作为唯一一道,因为并非每个请求都带该头部,也并非每个代理都会保留它。OWASP 的 CSRF 防护指南把 SameSite 视为多层中的一层,原因正在于此。

判断网络网关究竟证明了什么

在管理主机前放一份 VPN 出口白名单是很好的控制,却是很弱的身份声明。它证明请求来自 VPN 分配的地址,而这个地址可能被该出口上的所有人共用,它并不说明背后是哪个人或哪台设备。请把它当入口过滤器,而不是认证。

让它与请求级检查彼此独立。如果白名单是攻击者与改状态端点之间唯一的东西,一条配错的路由、一个 split-tunnel 客户端或一台借来的笔记本就会让控制全部失效。

也要记录来自预期范围之外的拒绝。它们的突增是有人正在探测你刚建好的边界的最有用信号,而沉默则说明过滤根本没有被触达。

把 Cookie、登录和授权写清楚

Cookie 的作用域决定大部分结果。父域 Cookie 会被送到管理主机,因此公开站点的脚本注入能以管理员会话行动;仅主机 Cookie 则不能。优先使用仅主机形式,加上 Secure 属性,并在平台支持时使用 __Host- 前缀,因为该前缀要求 Secure、Path=/ 且没有 Domain 属性。要读响应头而不是 Cookie 罐:Set-Cookie: __Host-admin_session=...; Secure; Path=/; HttpOnly; SameSite=Lax,没有 Domain 属性。浏览器会丢弃带 Domain 的 __Host- Cookie,所以仍在兄弟主机上出现的 Cookie,就证明该前缀从未生效。

然后把授权放在服务端,对每个请求、每个管理端点都做,而不只是登录时。来自允许网络的请求仍必须经过认证,并就其请求的操作获得授权;主机名不是行为主体。如果公开应用与管理应用共享登录流程或会话存储,请把这种关系写下来,因为那是从公开站点缺陷到管理操作的最短路径。

拆分之后重新检查边界

要跑负向测试,而不只是顺利路径:不带令牌的跨站表单必须被拒绝,来自其他会话的令牌必须被拒绝,任何 GET 都不得改变状态。在开发者工具里检查 Cookie 属性,确认不匹配的请求会触发 Origin 检查,并从 VPN 范围之外访问管理主机,看过滤是否生效。

这些都不会消除管理应用内部的 XSS 缺陷。独立主机名是有真实价值的浏览器侧隔离边界,但它不能替代防止不安全代码执行,也不能替代服务端请求防护。拆分之后,清单比看上去更短:独立的源、仅主机 Cookie、每次变更都有令牌、不当作身份的入口过滤,以及按请求判定的授权。

相关文章