关于 SOP(同源策略) / CORS(跨源资源共享) / CSRF(跨站请求伪造) 的一些认识

 

同源策略是指在Web浏览器中,允许某个网页脚本访问另一个网页的数据,但前提是这两个网页必须有相同的URI主机名端口号,一旦两个网站满足上述条件,这两个网站就被认定为具有相同来源。此策略可防止某个网页上的恶意脚本通过该页面的文档对象模型访问另一网页上的敏感数据。

跨域资源共享(英语:Cross-origin resource sharing,缩写:CORS),用于让网页的受限资源能够被其他域名的页面访问的一种机制。

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

0x00 SOP & CORS

浏览器中有的概念, 如果两个 URL 的 protocolport (如果有指定的话)和 host 都相同的话, 则这两个 URL 是同源.

浏览器默认会有同源策略, 用于限制一个 origin 下的页面与其他源的资源进行交互, 跨源的请求会受到限制.

CORS 的配置是由服务器响应中的Access-Control-Allow-xxx 等 Header 决定的, 主要限制了客户端通过 ajax 方式发起的请求.

比如在前后端分离的开发中, 通过地址 http://localhost:63343/ 访问前端, 前端中的 javascript 包含如下代码访问未配置 CORS 的后端接口(http://localhost:8080/user/auth).

1
2
3
4
5
6
7
8
9
const backend_url = "http://localhost:8080"
$.ajaxSetup({contentType: "application/json; charset=utf-8"});
$.post(backend_url + "/user/auth", JSON.stringify({
"token": token
}), function (data) {
// Do something
}).fail(function () {
// Do something
});

Chrome 报错, 请求失败.

Access to XMLHttpRequest at 'http://localhost:8080/user/auth' from origin 'http://localhost:63343' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

但也有的请求是被默认放行的, 具体来说只有符合下列规则的简单请求才能被暂时放行, 但如果服务器在返回的请求中未指明 Access-Control-Allow-Origin 或 允许的 Origin 不包含当前 Origin, 请求的响应将会被 Chrome 拦截, 不能被 javascript 获取, 但此时在服务器端请求已经完成. 如果请求包含了 Cookie 但响应头部中没有 Access-Control-Allow-Credentials: true, 请求的响应同样会被浏览器拦截.

简单请求

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求 满足所有下述条件,则该请求可视为“简单请求”:

  • 使用下列方法之一:
  • GET
  • HEAD
  • POST
  • 除了被用户代理自动设置的首部字段(例如 ConnectionUser-Agent)和在 Fetch 规范中定义为禁用首部名称的其他首部,允许人为设置的字段为 Fetch 规范定义的 CORS 安全的首部字段集合。该集合为:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制)
  • Content-Type的值仅限于下列三者之一:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded
  • 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 请求中没有使用 ReadableStream 对象。

(可以发现, 其实默认的 POST 表单是可以跨域的.(为了兼容性考虑.))

而非上述简单请求的复杂请求, 浏览器会发出一个预检请求来获取服务器的 CORS 配置.

如果未配置 CORS 或 CORS 允许的 Origin 中不包含当前 Origin, 浏览器将不会发出请求, 服务器也不能收到实际请求.

更多详细信息见: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

在 SpringBoot 后端开发时, 最简单的方法是在对应的 Controller 类上加上 @CrossOrigin 即可允许跨域.

1
2
3
4
5
@RestController
@CrossOrigin
public class UserController {
// ...
}

或者在 nginx 中配置 api 的反向代理到某个 path, 直接同源访问.

0x01 CSRF

CSRF 即利用浏览器自动传送 Cookie 的特性进行攻击.

例如用户在 A 网站上登录了自己的账号, 相关信息储存在 Cookie 中.

用户访问恶意的 B 网站, B 网站构造表单内容提交到 A 网站, 由于访问的是 A 的域名, 浏览器自动带上了 A 域名下的 Cookie, 服务器验证 Cookie 通过后完成 B 网站指定的操作, 而用户毫不知情. (CORS 默认允许跨域 POST 表单.)

传统的 CSRF 防御方式是要求在请求中包含 CSRF Token. CSRF Token 可以储存在本域的页面或通过 Cookie 获取. 即通过包含本域才能获取的信息来防止 CSRF.

也可以在服务器后端通过检测 Origin 和 Referer 来防止 CSRF.

前后端分离之后就无法直接在前端页面的 HTML 中包含 CSRF Token 了.

还是可以使用双重 Cookie 的方式 (Cookie 中包含 CSRF Token) 防御 CSRF.

但现在前后端分离的情况下大多使用 JSON 交换数据, 如果后端检验了 Content-Type 且设置了正确的Access-Control-Allow-Origin 即可以抵御 CSRF.(跨域的 JSON 请求会被 CORS 默认拦截.) 见: https://stackoverflow.com/questions/11008469/are-json-web-services-vulnerable-to-csrf-attacks

更加现代的用户鉴权方法是 JWT, 如果通过在请求头部添加 Token 鉴权, 本身就避免了 CSRF, 可以起到和双重 Cookie 同样的保护效果.

而对于仍然使用 Cookie 鉴权的场景, 现在也已经有了 CSRF 的终极解决方案: SameSite cookies.

直接从根源上解决了 CSRF 的问题, 即不再在跨域请求中传送 Cookie, 且近期版本的浏览器将 Lax 作为默认值, 已经可以防御 CSRF 攻击了.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.site/post/cors-and-csrf/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬