为什么将 API 设计为只接受 JSON 格式(Content-Type: application/json)可以降低 CSRF 风险?
将 API 设计为只接受 JSON 格式(即强制要求 HTTP Header 中包含 Content-Type: application/json),确实可以显著降低 CSRF(跨站请求伪造)的风险。
这背后的核心原因在于:浏览器的安全机制(同源策略和 CORS)对“简单请求”和“非简单请求”的处理方式不同。
以下是详细的技术原理解析:
1. 传统的 CSRF 攻击依赖于 HTML Form
传统的 CSRF 攻击通常利用 HTML 的 <form> 标签。
攻击者会在恶意网站上构造一个表单,诱导受害者点击,或者通过脚本自动提交:
<!-- 传统攻击方式 -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>
HTML 表单只支持三种 Content-Type:
application/x-www-form-urlencoded(默认)multipart/form-data(用于文件上传)text/plain
关键点: HTML 表单无法设置 Content-Type 为 application/json。
如果你的 API 严格检查 Content-Type 必须是 application/json,那么上述通过表单发起的攻击请求会被服务器直接拒绝(通常返回 415 Unsupported Media Type),因为请求头不匹配。
2. 触发 CORS 预检请求 (Preflight Request)
既然 HTML 表单做不到,攻击者如果要发送 Content-Type: application/json,就必须使用 JavaScript(如 XMLHttpRequest 或 fetch API)。
当攻击者在恶意网站(evil.com)通过 JS 向你的网站(bank.com)发起请求,并试图设置 Content-Type: application/json 时,会触发浏览器的 CORS(跨域资源共享) 机制。
根据 CORS 规范,请求分为两类:
- 简单请求 (Simple Request): 使用 GET/HEAD/POST 方法,且 Content-Type 是上述表单支持的三种之一。浏览器会直接发送请求(带上 Cookie)。
- 非简单请求 (Non-simple Request): 使用了自定义 Header,或者 Content-Type 为 application/json。
防御流程如下:
- 攻击者的 JS 试图发送 JSON 请求。
- 浏览器发现这是一个“非简单请求”(因为 Content-Type 是 JSON)。
- 浏览器暂停发送真实的业务请求。
- 浏览器先自动发送一个 OPTIONS 请求(预检请求,Preflight)给服务器,询问:“你允许
evil.com发送 JSON 格式的 POST 请求吗?” - 你的服务器(如果没有错误配置 CORS)会发现来源是
evil.com,于是拒绝或者不返回允许的 Header。 - 浏览器收到预检失败的结果,彻底拦截那个真实的 JSON POST 请求。
结论: 攻击者的请求甚至无法到达你的业务逻辑层,在浏览器层面就被拦截了。
3. 为什么说只是“降低”风险,而不是“彻底根除”?
虽然强制 JSON 格式非常有效,但它不能作为唯一的防御手段,原因如下:
- CORS 配置错误: 如果你的服务器端 CORS 配置过于宽松(例如
Access-Control-Allow-Origin: *且允许了携带凭证Access-Control-Allow-Credentials: true),那么上述的“预检保护”就会失效,攻击者依然可以跨域发送 JSON 请求。 - 历史遗留漏洞(Flash/Java): 虽然现代浏览器已经淘汰了 Flash 和 Java Applet,但在过去,这些插件有时可以绕过浏览器的 Header 限制发送 JSON 请求。虽然现在不用担心这个,但在安全理论上这是一个缺口。
- Content-Type 嗅探或解析漏洞: 如果某些后端框架在处理请求时比较“宽容”,比如它虽然宣称只接受 JSON,但如果攻击者发送
text/plain格式的有效 JSON 字符串,后端依然解析并执行了,那么攻击者就可以利用<form enctype="text/plain">进行攻击。因此,后端必须严格校验 Header,不仅仅是尝试解析 Body。
总结
将 API 设计为只接受 Content-Type: application/json 可以防御 CSRF,是因为:
- HTML 表单无法伪造这种请求头。
- 通过 JS 发起这种请求会强制触发 CORS 预检,只要服务器不通过 CORS 显式允许攻击者的域名,浏览器就会阻止请求发送。
最佳实践建议:
尽管这很有效,依然建议采用 深度防御 (Defense in Depth) 策略:
- 强制使用 JSON。
- 严格配置 CORS。
- 依然使用 CSRF Token(双重 Cookie 验证或同步 Token 模式)作为核心防御。
- 设置 Cookie 的
SameSite属性为Lax或Strict。