详解浏览器跨域访问的几种办法

1,134 阅读12分钟

​​​​​​​​​​​​​​摘要: 本文讨论 web 前端安全问题以及应对措施,浏览器同源策略以及对资源跨域访问的几种解决方案

本文分享自华为云社区《Web安全和浏览器跨域访问》,原文作者:kg-follower 。

今天说一说和前端相关的 Web 安全问题和开发过程中经常遇到的跨域问题。

1.Web 安全

1.1 XSS

基本原理

XSS(Cross-Site Scripting),跨站脚本攻击通过在用户的浏览器内运行非法的 HTML 标签或 JavaScript 进行的一种攻击。

攻击手段

攻击者往 Web 页面里插入恶意网页脚本代码,当用户浏览该页面时,嵌入 Web 页面里面的脚本代码会被执行,从而达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。

XSS 攻击分类

反射型 xss 攻击。通过给被攻击者发送带有恶意脚本的 URL 或将不可信内容插入页面,当 URL 地址被打开或页面被执行时,浏览器解析、执行恶意脚本。

反射型 xss 的攻击步骤:1. 攻击者构造出特殊的 URL 或特殊数据;2. 用户打开带有恶意代码的 URL 时,Web 服务器将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器;3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行;4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

防御:1.Web 页面渲染的所有内容或数据都必须来自服务端;2. 客户端对用户输入的内容进行安全符转义,服务端对上交内容进行安全转义;3.避免拼接 html。

存储型 xss。恶意脚本被存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回浏览器去执行。

存储型 xss 的攻击步骤:1. 攻击者将恶意代码提交到目标网站的数据库中;2.用户浏览到目标网站时,前端页面获得数据库中读出的恶意脚本时将其渲染执行。

防御:防范存储型 XSS 攻击,需要我们增加字符串的过滤:前端输入时过滤;服务端增加过滤;前端输出时过滤。

通常有三种方式防御 XSS 攻击:1. ContentSecurity Policy(CSP)。CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。通常可以通过两种方式开启,例如只允许加载相同域下的资源:

设置 HTTP Header 中的 CSP(Content-Security-Policy: default-src 'self')

设置 meta 标签的方式(<meta http-equiv="Content-Security-Policy"content="form-action 'self';">)

转义字符

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义:

function escape(str) {
  str = str.replace(/&/g, '&')
  str = str.replace(/</g, '<')
  str = str.replace(/>/g, '>')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, ''')
  str = str.replace(/`/g, '`')
  str = str.replace(/\//g, '/')
  return str
}

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法:

const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
console.log(html)
<h1>XSS Demo</h1><script>alert("xss");</script>

经过白名单过滤,dom 中包含的

HTTP-only Cookie

禁止 JavaScript 读取某些敏感 cookie,使得 cookie 只有 http 能够访问。

1.2 CSRF

基本概念

CSRF(Cross-site request forgery 跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

CSRF 攻击类型

主动型攻击。用户访问网站 A 并在浏览器保存 A 的登录状态(cookie 等信息),攻击者诱导受害者访问网站 B,网站 B 含有访问 A 接口的恶意代码,受害者访问 B 时带着 A 的登录状态,攻击者便可以冒充用户执行对 A 的恶意操作。

被动型攻击。攻击者在网站 A 发布带有恶意链接的评论或内容(提交对 A 带有增删改的诱导型标签),当其他拥有登录状态的受害者点击评论的恶意链接时,就会冒用受害者登录凭证发起攻击。

CSRF 攻击防范

验证 HTTP Referer 字段。在 HTTP 头中有 Referer 字段,他记录该 HTTP 请求的来源地址,如果跳转的网站与来源地址相符,那就是合法的,如果不符则可能是 csrf 攻击,拒绝该请求。

SameSite。可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击。

**请求中加入 token。**服务端给用户生成一个 token,加密后传递给用户,用户在提交请求时,需要携带这个 token,服务端发现 token 不存在或者 token 校验不成功,那么就拒绝该请求。

1.3 流量劫持

DNS 劫持

DNS 劫持就是通过劫持了 DNS 服务器,通过某些手段来取得某个域名的解析控制权,进而修改此域名的解析结果,导致对该域名的访问由原 IP 地址转入到修改后的 IP,其结果就是对特定的网站不能访问或访问的是假网址。

防御:使用 https 校验通信双方身份和数据完整性。

点击劫持

攻击者构建了一个非常有吸引力的网页,将被攻击的页面放置在当前页面的 iframe 中,使用样式将 iframe 叠加到非常有吸引力内容的上方,将 iframe 设置为 100%透明,其实就是通过覆盖不可见的页面,诱导用户点击而造成的攻击行为。

防御措施。1. X-FRAME-OPTIONS 设置允许 iframe 加载的域 2. 限制 iframe 页面中的 JavaScript 脚本执行。

无论是 xss、csrf 还是点击劫持,上面讨论的这几种攻击属于前端攻击,原因大多是开发者的脚本或模板代码存在不安全的隐患或是没有考虑网络传输安全问题。下面简单说一说恶意攻击利用网站后台漏洞发起的攻击。

1.4 SQL 注入

SQL 注入漏洞存在的原因,就是拼接 SQL 参数。也就是将用于输入的查询参数,直接拼接在 SQL 语句中,恶意攻击者可以构造特殊的 sql 语句绕过安全验证。

SQL 注入条件:1.攻击者可以控制输入的数据;2.服务器要执行的代码拼接了被控制的数据。

SQL 注入防御。1. 严格限制 Web 应用的数据库的操作权限;2. 对进入数据库的特殊字符(’,”,,<,>,&,*,; 等)进行转义处理,或编码转换,类似防御 xss 攻击时对输入转义;3. 所有的查询语句建议使用数据库提供的参数化查询接口,如使用占位参数或对象关系映射 ORM。

1.5 DDOS 攻击

DOS 攻击通过在网站的各个环节进行攻击,使得整个流程跑不起来,以达到瘫痪服务为目的。最常见的就是发送大量请求导致服务器过载宕机。DDOS 攻击的原理就是利用分布式的客户端,向目标发起大量看上去合法的请求,消耗/占用大量资源,从而达到拒绝服务的目的。

攻击方式:1.端口扫描;2.ping 洪水;3.SYN 洪水;4.FTP 跳转攻击;

DDOS 防范。1.在服务器上删除未使用的服务,关闭未使用的端口。2. 进行实时监控,封禁某些恶意密集型请求 IP 段;3. 进行静态资源缓存,隔离源文件的访问,比如 CDN 加速;4. 隐藏服务器的真实 IP 地址

2. 跨域和同源策略

同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。所谓同源是指“协议+域名+端口”三者均相同。

同源策略限制了客户端 js 代码的以下行为:

1.Cookie、LocalStorage 和 IndexDB 无法读取;

2.DOM 节点。来自一个源的 js 只能读写自己源的 DOM 树不能读取其他源的 DOM 树。如果两个网页不同源,就无法拿到对方的 DOM。典型的例子是 iframe 窗口和 window.open 方法打开的窗口,它们与父窗口无法通信。

网站不开启同源策略,钓鱼网站便可以使用 iframe 标签加载中国银行登录界面,执行脚本进而拿到用户名密码。

当设置了同源策略,父子窗口执行获取对方 DOM 时会报错。

3.AJAX 请求限制

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

除了架设服务器代理,还有以下几种方法规避同源限制:JSONP,WebSocket,CORS,本文详细讨论下后两种方法的实现。

**WebSocket。**WebSocket 是一种通信协议,使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。Websocket 请求头信息包含一个 origin 字段,服务器根据这个字段判断是否允许本次通信。

CORS。CORS 跨域资源共享是 W3C 标准,是解决跨域 Ajax 请求的最常见解决方法。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。只要同时满足以下两大条件,就属于简单请求:

(1) 请求方法是以下三种方法之一:HEAD、GET、POST

(2)HTTP 的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限于三个值
application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段,该字段用来说明,本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。若该响应的头信息没有包含
Access-Control-Allow-Origin 字段,就抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。若 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。其中 Access-Control-Allow-Origin 字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个*,表示接受任意域名的请求。

对于非简单请求,在正式通信之前,会增加一次 HTTP 查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

"预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。

除了 Origin 字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method。该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法

(2)Access-Control-Request-Headers。该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。

预检请求的回应。

服务器收到"预检"请求以后,检查了 Origin、
Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。回应最关键的是 Access-Control-Allow-Origin 字段,表示允许该源的请求,若没有任何 CORS 相关头信息字段则说明服务器否认该请求。若服务器允许,则 Access-Control-Allow-Methods 字段是必须的,它的值是一个逗号分隔的字符串,表明服务器支持的方法。如果预检请求包含 Access-Control-Request-Headers 字段,则返回体中该字段也是必须的,它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。预检请求得到允许回应后,浏览器便发送正常 CORS 请求。

最近在开发一个前端 poc 项目时遇到了跨域资源访问被限制的问题,在本地启动 angular 项目,其他人可以通过 ip 访问到静态资源,发送 ajax 请求时被限制。于是想通过配置代理的方式解决这个跨域问题:在和 package.json 同级的目录中新建 proxy.conf.json 文件,target 字段是后端服务真实的 ip,changeOrigin 字段设置为 true,关闭 secure 字段。

{
    "/": {
      "target": "http://10.173.99.224:8081/",
      "changeOrigin": true,
      "secure": false,
      "loglevel": "debug"
    }
}

​在 package.json 的启动命令中添加

 "scripts": {
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json --host 0.0.0.0",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },

--host0.0.0.0 表示监听所有来源的主机。解决

点击关注,第一时间了解华为云新鲜技术~