登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go 服务怎么用 Sec-Fetch-Site 拦住跨站请求:Fetch Metadata 的最小中间件

来源:17golang原创

时间:2026-07-17 12:33:14 349浏览 收藏

有一次给后台的 POST /api/billing/plan 加 Cookie 登录后,接口测试和站内页面都正常,直到安全演练用第三方页面提交了一次表单。业务代码没有报错,浏览器却把带着登录态的请求送到了服务端。CSRF token 当然还要保留,但这里还可以多看一眼浏览器已经附带的 Sec-Fetch-Site:它能告诉服务端,这次请求来自同源、同站,还是另一个站点。

要点速览

  • Sec-Fetch-Site: cross-site 适合触发敏感写操作的额外拦截,不代表用户未登录,也不能替代权限判断。
  • 中间件应先放行 same-originsame-site 和明确允许的公开端点,再对跨站的状态改变请求返回 403。
  • 缺少 Fetch Metadata 的浏览器、脚本客户端和健康检查不能一刀切拒绝;这条分支应继续走 CSRF token、Origin 或鉴权校验。
  • 按请求头做出不同决定时,响应要包含 Vary: Sec-Fetch-Site,避免缓存把一个用户的结果交给另一个请求上下文。

先看清 Sec-Fetch-Site 解决的是哪一段风险

很多服务只在写接口里校验 Cookie 和用户角色。它们能回答“谁在调用”,却不一定能回答“浏览器是从哪里发起的”。Fetch Metadata 是浏览器在请求层提供的一组上下文信号,Sec-Fetch-Site 其中最适合先落地:same-origin 表示协议、主机和端口相同;same-site 表示仍在同一站点范围;cross-site 则来自不同站点;地址栏直接打开或书签触发的导航可能是 none

这不是身份凭据,也不是 CORS 替身。前端脚本不能自己伪造 Sec- 前缀的请求头,但命令行客户端、老浏览器、网关回放和自动化探针可能根本不携带它。因此更合适的做法是把它当作一层资源隔离闸门:带头且为 cross-site 的敏感写请求优先拦下;没有头时,不凭空放行,而是回到原有的 CSRF 和鉴权检查。

请求特征中间件动作后续仍要检查
same-originsame-site交给业务处理会话、角色、CSRF token
cross-site + POST/PUT/PATCH/DELETE默认 403,除非端点被明确列为跨站公开接口白名单是否过宽
none + GET/HEAD可作为直接导航继续处理页面本身的鉴权与跳转
请求头缺失不靠该字段拒绝,进入兼容路径CSRF、Origin、认证和限流
Go 服务根据 cross-site 与 same-origin 请求状态决定 403 拒绝或继续处理的低饱和问题修复对照图
同一条写接口,跨站请求先停在网关闸门;同源请求则继续进入已有鉴权和业务流程。

把跨站写请求拦在业务处理前

当时最容易犯的错是直接对所有 cross-site 返回 403。这样确实省事,却会把支付回调、公开嵌入资源或专门设计给第三方的接口一并打断。先把“状态改变”与“允许跨站”的端点拆开,策略会稳很多。下面的示例默认只拦截带有 cross-site 的非安全方法;/hooks/ 是演示中的明确例外,实际项目应换成自己的端点与签名校验。

package gate

import (
	"net/http"
	"strings"
)

func FetchSiteGate(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		site := r.Header.Get("Sec-Fetch-Site")
		addVary(w, "Sec-Fetch-Site")

		if site == "cross-site" && changesState(r.Method) && !strings.HasPrefix(r.URL.Path, "/hooks/") {
			http.Error(w, "cross-site request blocked", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func changesState(method string) bool {
	return method != http.MethodGet && method != http.MethodHead && method != http.MethodOptions
}

func addVary(w http.ResponseWriter, value string) {
	old := w.Header().Get("Vary")
	if old == "" {
		w.Header().Set("Vary", value)
		return
	}
	w.Header().Set("Vary", old+", "+value)
}

代码刻意没有看到 site == "" 就拒绝。这个判断只能说明浏览器没有发送该提示,无法说明请求一定恶意。先让请求进入后续的 CSRF、Origin 和认证链,既能兼容 API 客户端,也不会把安全责任悄悄移到一个并非所有用户代理都支持的字段上。

为什么 CORS 通过了,cross-site 仍可能要拒绝

CORS 解决的是“浏览器是否允许脚本读取响应”的协商问题;Fetch Metadata 更像服务端对资源用途做的一次早期判断。一个接口即便已经允许特定前端域名跨源读取,也不等于所有跨站的 Cookie 写请求都应该进入业务层。这里别急着把 CORS 白名单塞进通用中间件,先列出真正需要跨站使用的 API,再为它们单独配置来源、方法、凭据和签名校验。

还有一个常被漏掉的细节是缓存。若同一个 URL 会因 Sec-Fetch-Site 得到不同结果,响应应携带 Vary: Sec-Fetch-Site。示例里的 addVary 没有覆盖已有字段,避免把 Accept-Encoding 等缓存维度抹掉。反向代理或 CDN 有自定义缓存规则时,也要确认它们会尊重这一维度;否则本来只给同源请求的内容,可能被错误复用。

请求头缺失时,回到 CSRF 与 Origin 的组合校验

上线前最需要验证的是“缺失请求头”这条路径。移动端、服务间调用、测试脚本以及部分浏览器都可能不带 Sec-Fetch-Site。如果中间件只靠它做准入,攻击者换一个普通 HTTP 客户端就绕开了策略;如果一律拒绝,又会误伤自己的 webhook、探针和自动化任务。

我更建议把它放在已有防线之前:有 cross-site 且属于敏感写操作,就尽早返回 403;请求头缺失或允许继续的请求,再验证会话、CSRF token,以及适合该端点的 Origin/Referer 约束。对机器回调接口,优先用专用凭据、时间戳和签名,而不是依赖浏览器头。

Go 服务在 no-header 请求下继续经过 CSRF、Origin 和 allow 校验链的低饱和工程场景图
Fetch Metadata 缺失不是放行理由;它只是把请求交回 CSRF、Origin 与接口专用认证这条兼容链。

拿四组请求做一次回归,比只看 403 更可靠

在预发布环境里,可以用浏览器网络面板观察真实字段,再用 Go 的 httptest 覆盖中间件。以下四组足够发现多数策略写反的问题:

  • 站内页面对 POST /api/billing/plan 发请求,携带 same-origin,应继续到业务层。
  • 外部站点的表单或脚本发同一写请求,携带 cross-site,应收到 403,且业务审计日志不应出现状态修改。
  • 浏览器直接访问一个需要展示的 GET 页面,若字段为 none,不应被写接口策略误伤。
  • 模拟不带该字段的服务端调用,确认它没有因为空头直接失败,同时仍必须通过接口自己的认证或签名校验。

如果团队有跨域 SDK、嵌入式页面或第三方 webhook,别把它们藏在全局例外里。把端点、允许来源、凭据方式和负责人写成一张短清单。过几个月回头看,通常会发现“临时放行”的接口才是最需要收紧的地方。

相关问题

Sec-Fetch-Site 能完全代替 CSRF token 吗?

不能。它适合在浏览器携带字段时提供额外的请求上下文,但并非所有客户端都有该字段。Cookie 会话的写操作仍应保留合适的 CSRF 防护与权限校验。

same-site 就一定安全可靠吗?

也不能这样理解。same-site 只是站点关系信号,子域、跳转链和业务授权仍要分别评估。敏感接口不能只凭一个头决定用户能做什么。

API 给移动端和服务端调用时怎么处理?

不要强制要求 Fetch Metadata。为机器调用使用 token、mTLS、签名或其他明确的服务端身份机制,并把该路径和浏览器 Cookie 会话区分开。

为什么响应里要有 Vary: Sec-Fetch-Site?

因为同一资源会按这个请求头得到不同结果。Vary 让缓存知道不能把一种上下文生成的响应直接复用给另一种请求上下文。

把它当作早期闸门,而不是唯一防线

这层 Go 中间件最有价值的地方,是在业务处理之前把明显的跨站写请求挡住。它不替代 CSRF token,也不替代 Origin、签名和权限控制;它把原本分散在日志里的“请求从哪来”变成了一条可观察的服务端决策。先从一两个 Cookie 写接口试运行,记录被拒绝的路径,再逐步收紧例外范围,通常比一次性全站拦截更稳。

字段语义、缓存提示和兼容建议可继续参考 MDN 的 Fetch metadataSec-Fetch-Site,以及 web.dev 的 资源隔离实践

[] []
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>