同源策略与跨域方法与跨域攻击

5 阅读5分钟

同源策略

源: 协议(http,https), 主机(域名如example.com或www.example.com,注意pan.baidu.com和zhidao.baidu.com不算同一个主机 / IP地址 / 本地主机名) , 端口(80,8083)

源的继承: 用about:blank或javascript:的方式打开的网页由于没有源所以会继承打开这个页面的网页的源

//about:blank
const newWin = window.open('about:blank');
newWin.document.write('<script>console.log(location.origin);</script>');
//javascript:
<a href="javascript:alert(location.origin)">点击</a>

只有当协议,域名,端口均相同的时候才算是同源

作用: 如果没有同源策略,恶意网站可以通过iframe,XHR等读取你在其他网站的敏感信息,比如向淘宝发起请求,由于浏览器会在发请求的时候从本地找这个请求地址的Cookie,如果有的话携带上,所以可以通过这种方式攻击你

内容

同源策略保护的是脚本对数据的读取

注意同源策略只存在于浏览器,所以nginx服务器才能不受同源影响

  1. 不同源的脚本不能通过iframe等方式相互读取对方的DOM

  2. Web API限制 :

    1. XHR和fetch能发送跨域请求但不能读取响应内容(除非服务器支持CORS)
    2. Web Storage(localStorage,sessionStorage)和IndexDB无法跨域读取
    3. Cookie无法跨源读取
  3. 网络请求限制: 某些跨域请求(图片,CSS,JS,提交表单,重定向等)本身不会被浏览器拦截,但脚本无法读取内容,即图片可以显示在页面上但是脚本无法获取图片的像素信息等,js可以执行但脚本无法获取js的内容

  4. 注意这里说能发送跨域请求时简单请求能发送,如果是有自定义头或者PUT这种非简单请求会先发送一个OPTIONS预检请求,如果服务器返回的是200或204并携带允许的域名等范围就可以继续发送真正的请求

    简单请求(同时满足以下条件):

    1. 方法:GETPOSTHEAD
    2. 只能使用以下请求头:Accept(告诉服务器客户端能处理哪些MIME类型),Accept-Language(声明客户端偏好的语言类型如zh-CN,en-US),Content-Language(指示响应内容面向的语言,一般用作响应头,但也可以用作请求头表示请求体语言,但是很少使用),Content-Type且不能有自定义请求头
    3. Content-Type 只能是:application/x-www-form-urlencoded(即表单数据编码(键值对,如 a=1&b=2)),multipart/form-data(即文件上传),text/plain(即纯文本)

跨域问题解决:

  1. JSONP: script标签不受同源策略影响,所以可以发送一个script标签,用src来发送请求并带上一个回调函数名,即

    const script = document.createElement('script');
    script.src = 'https://api.example.com/data?callback=handleData';
    document.body.appendChild(script);
    

    这个回调函数是在发送方自己的代码里定义好的,然后服务端接收到这个请求发现回调函数的名字是handleData,于是把数据当参数传给回调函数即handleData({name: '老王'}),然后这个返回来的JS代码就会被执行(函数是发送方已经定义好的)

    1. 内容:

      <script>
              // 定义回调函数,接收数据
              function showUser(data) {
                  console.log('收到用户数据:', data);
                  document.body.innerHTML += `<p>姓名:${data.name}</p>`;
              }
      ​
              // 创建 script 标签,向 api.com 请求数据,并带上回调函数名
              const script = document.createElement('script');
              script.src = 'https://api.com/user?callback=showUser';
              document.body.appendChild(script);
              // 当 script 加载完成后,服务器返回的 JavaScript 代码会被执行
          </script>
      

      然后服务端返回showUser({"name": "张三", "age": 30});

    2. 优点: 兼容性好(所有浏览器包括IE6),无需服务器特殊配置,实现简单

      缺点: 只支持GET, 安全风险高, 一旦携带凭证容易被滥用, 污染全局作用域(需要定义全局函数) , 错误处理困难

    3. 只能用于GET,因为JSONP本质上是用script标签的src属性获取资源,所以只能是get请求

    4. 返回的内容必须是JS代码,因为script标签加载到的资源浏览器会当作JS代码执行,所以如果是JSON浏览器会当作JS执行语法报错,所以必须返回可执行的JS代码比如handleData({"name": "John", "age": 30});

    5. script标签的请求能发出去,返回的代码能执行,但是脚本无法读取,但是注意这不是同源策略的限制而是浏览器本身就不允许暴露外部脚本的源代码给DOM或JS,也就是说

    6. 问题:

      如果服务器被攻破,返回恶意代码就会盗取网页的Cookie等

      如果服务器没有严格校验函数名,攻击者可能构造恶意函数名比如steal(document.cookie)(注意这个代码会执行steal函数,所以如果页面里根本没有steal函数js会报错,一般是构造页面里已有的恶意函数)

      CSRF风险

  2. CORS: 服务器通过添加HTTP头来告诉浏览器允许哪些跨域请求

    1. 浏览器跨域请求分为简单请求和预检请求两种

      简单请求: 浏览器会直接发送请求并带上Origin: 发送方地址,服务器响应必须包含Access-Control-Allow-Origin: 允许的源的地址 头,否则会报错(注意是fetch进入catch或XHR触发error事件,无法拿到响应内容,不是请求被拦截),注意这里允许的源的地址可以是通配符,但是如果要携带Cookie就不允许是通配符,如果是通配符就会报错

      预检请求: 非简单请求浏览器会先发送一个预检请求,请求头包含

      Origin: https://myapp.com
      Access-Control-Request-Method: PUT
      Access-Control-Request-Headers: X-Token, Content-Type
      

      服务器需要明确在预检响应中允许这些方法和头

      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: https://myapp.com
      Access-Control-Allow-Methods: GET, POST, PUT, DELETE
      Access-Control-Allow-Headers: X-Token, Content-Type
      缓存时间,避免每次都预检
      Access-Control-Max-Age: 86400
      

      浏览器收到同意的预检响应之后会发送真正的请求

    2. 优点: 官方标准现代浏览器均支持,功能全面支持所有HTTP方法(GET,POST等)和Cookie和自定义头,性能好(简单请求无额外开销,预检请求可缓存), 安全性好(服务器可以精准控制源,方法,头等)

      缺点: 需要后端配合,所以第三方api用不了, 预检请求会增加开销, 配置复杂

    3. 服务器配置CORS

      node.js

      app.use((req, res, next) => {
        // 允许所有源(生产环境建议指定具体源)
        res.setHeader('Access-Control-Allow-Origin', '*');
        // 允许的方法
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        // 允许的请求头
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
        // 如果需要携带 Cookie
        // res.setHeader('Access-Control-Allow-Credentials', 'true');
        // 预检请求直接返回 204
        if (req.method === 'OPTIONS') {
          return res.sendStatus(204);
        }
        next();
      });
      
  3. 反向代理:

    1. 实现:

      比如nginx反向代理,相当于是客户端发送请求给nginx,然后nginx再向服务端发请求

      location /api/ {
              # 后端服务地址(根据实际情况修改)
              # 如果后端在 Docker 容器中且与 nginx 在同一网络,可用容器名:端口
              proxy_pass http://后端的服务名:8080;
              # 传递原始请求头,让后端获取真实信息
              #把浏览器中输入的域名(输入的是ip的话那就是ip,总的就是等于请求头中的Host字段)原封不动的传给后端服务器
              proxy_set_header Host $host;
              #将客户端的真实IP传给后端
              proxy_set_header X-Real-IP $remote_addr;
              #每经过一层代理就把IP地址追加到列表末尾
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      
    2. 优点: 前端只需请求相对路径, 后端无需配置CORS, 代理层可以实现限流缓存等, 可以代理多个后端服务而对外只暴露一个域名

      缺点: 部署复杂, 多一次代理转发, 仅适用于HTTP/HTTPS

  4. 开发期间的跨域代理

    注意这个没法用到生产环境,因为这俩的配置本质上是开发服务器,所以生产打包后就没有了

    1. 用vite配置跨域

      server: {
          proxy: {
            '/api/': {
              target: 'http://localhost:8080',
              changeOrigin: true,//意思是发请求的时候把请求头的Host字段改成目标服务器的域名/端口,同时也会修改Orign头,如果存在的话(即http://localhost:8080)
              rewrite: path => path.replace(/^/api//, '')//重写路径,这里是把匹配到的api开头去掉,如果后面参数写newApi就可以发送到http://localhost:8080/newApi/user,如果这句直接不写就会默认发送到http://localhost:8080/api/user
            },
          },
        },
      
    2. 用webpack配置跨域:

      module.exports = {
        devServer: {
          proxy: {
            '/api': {
              target: 'http://otherdomain.com',
              changeOrigin: true,
              pathRewrite: { '^/api': '' }
            }
          }
        }
      };
      

    跨域攻击

    1. CSRF: 由于同源策略只限制读取,并不限制发送,所以无法约束CSRF,CSRF攻击是利用已登录的网站浏览器发请求的时候会携带Cookie这一特性来进行攻击,主要用于篡改,不用于窃取

      解决方法: 设置Cookie的SameSite属性为Strict可以禁止第三方站点发起请求的时候携带Cookie,但是这样一来点击链接跳转页面的时候也会丢失Cookie,影响用户体验 / SameLite=Lax+CSRF Token,即服务端生成一个随机字符串,用户在提交表单的时候提交这个token,服务端进行校验

    2. XSS: 跨站脚本攻击,注意这个其实本质上不依赖跨域,因为往往就是在目标页面注入的,比如在某个网站找到漏洞植入

      解决方法: 对用户输入进行严格过滤和转义(输出编码)

    3. CORS配置不当: 比如配置成通配符*或动态反射用户输入的Orign而不加校验,这会导致恶意请求不但能请求还能读取

      此外,配置成null也很危险,about:blank会继承发起的页面的源,但是如果是直接在页面输入about:blank那他的源就是null

      还有iframe当设置了sandbox但是sandbox不包含allow-same-origin的时候源会被设置为null

    4. JSONP劫持: 如果某个接口使用了JSONP回调,例如 https://api.com/user?callback=jsonHandler,攻击者可以在自己的页面构造 <script src=“https://api.com/user?callback=stealData”>。当用户访问攻击者页面时,用户相当于在攻击者页面的上下文中,以登录态执行了该JSONP接口,返回的数据会流入攻击者定义的 stealData 函数中,导致数据泄露。注意现在已经基本失效,因为Lax下script标签根本不会携带Cookie,除非手动把SameLite设置成None

      解决方法: 不使用JSONP