浏览器

# 浏览器安全

# XSS攻击

跨站脚本攻击(Cross-Site Scripting,为了与 CSS 区分所以被称作 XSS),攻击者通过在网站注入恶意脚本,注入方式包括但不限于JavaScript/CSS/Flash,使之在用户的浏览器上运行,从而盗取用户信息如cookie等。本质:网站没有对恶意代码进行过滤,与正常代码混合在一起,浏览器无法分辨导致恶意代码的执行。

攻击者通过XSS可进行以下操作:

  • 获取页面的数据,如DOM,cookie,localStorage
  • DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器
  • 破坏页面结构
  • 流量劫持(将连接指向某网站)

# 攻击类型

  1. 存储型(存在数据库中)

    • 恶意脚本提交存储在目标服务器的数据库中,当浏览器请求数据时,脚本从服务器传回并执行。
    • 常见于需要用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
  2. 反射型(存在URL中)

    • 攻击者诱导用户访问带有恶意代码的URL,服务端将恶意代码从URL取出,拼接在HTML中返回给浏览器,浏览器解析执行这段XSS代码后当作脚本执行,最终完成XSS攻击。
    • 该攻击常见于通过URL传递参数的功能,如网站搜索、跳转等。
    • 该攻击需要用户主动打开恶意的URL才能生效。

    image-20220125195126140

  3. DOM型

    • 通过修改页面的DOM节点形成XSS。
    • 恶意代码存储在URL中,取出和执行恶意代码都由浏览器完成,属于前端自身的安全漏洞。

    image-20220312105801248

# 防御XSS攻击

  1. 使用纯前端的方式,不用SSR;对插入到HTML中的代码做好充分的转义,如将"<"转义成"lt;"。
  2. 对于DOM型攻击,在数据获取渲染和字符串拼接时对可能出现的恶意代码情况进行判断。
  3. 使用CSP,CSP本质是建立一个白名单,告诉浏览器哪种外部资源可以加载和执行。有以下两种方式:
    • 设置HTTP首部中的Content-Security-Policy
    • 设置meta标签<meta http-equiv="Content-Security-Policy">
  4. 对一些敏感信息进行保护,比如cookie使用http-only,使脚本无法获取;也可以使用验证码,避免脚本伪装成用户执行一些操作。

如果有一个用户需求,必须动态生成DOM:

  1. String->DOM:使用new DOMParser()这个API时要注意对字符串进行转义
  2. 上传svg文件:svg允许包含JS代码,要注意XSS攻击
  3. 自定义跳转链接:要做好过滤
  4. 自定义样式:如设置单选框选中的自定义样式时通过background的url发送get请求,暴露服务器

# CSRF攻击

CSRF跨站请求伪造攻击:攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。

本质:利用cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。有以下三种类型:

<!--1. 链接类型-->
<a href="https://bank.com/transfer?to=hacker&amount= 100">点我抽奖</a>

<!--2. get类型-->
<img style="display:none;" src="https://bank.com/transfer?to=hacker&amount=100"/>

<!--3. post类型-->
<form action="https://bank/transfertons_of_money" method="POST">
  <input name="amount" value="1000000000000" type="hidden" />
  <input name="to" value="hacker" type="hidden" />
</form>
1
2
3
4
5
6
7
8
9
10
11

# 防御CSRF攻击

  1. 进行同源检测:服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
  2. 使用CSRF Token进行验证:服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
  3. 对Cookie进行双重验证:服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效,因为可以通过脚本获取cookie。同时这种方式不能做到子域名的隔离。
  4. 在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式(strict)下,cookie在任何情况下都不可能作为第三方Cookie使用;在宽松模式(lax)下,跨站中的连接和get请求会携带cookie,其他不会。SameSite Cookie 是网络安全发展历程中的重要一环,不仅起到了防御 CSRF 的作用,对用户的隐私保护也十分关键。此外,正确理解 SameSite 的作用对于前端工程师的日常开发工作而言同样十分重要,避免踩坑(诸如:为什么登录失败)。

# 中间人攻击

攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。攻击过程如下:

  1. 客户端发送请求到服务端,请求被中间人截获
  2. 服务器向客户端发送公钥
  3. 中间人截获公钥,保留在自己手上。然后自己生成⼀个伪造的公钥,发给客户端
  4. 客户端收到伪造的公钥后,生成加密hash值发给服务器
  5. 中间人获得加密hash值,用自己的私钥解密获得真秘钥,同时生成假的加密hash值,发给服务器
  6. 服务器用私钥解密获得假密钥,然后加密数据传输给客户端

image-20220312144202114

# 防御中间人攻击

使用HTTPS进行传输,HTTPS有以下三大特性:

  1. 可靠性:加密
  2. 完整性:MAC验证
  3. 不可抵赖性:数字签名

下面TLS是1.2版的。

image-20220312150200803

# 网络劫持

  1. DNS劫持: (输入京东被强制跳转到淘宝这就属于dns劫持),涉嫌违法,已被监管起来
    • DNS强制解析: 通过修改运营商的本地DNS记录,来引导用户流量到缓存服务器
    • 302跳转的方式: 通过监控网络出口的流量,分析判断哪些内容是可以进行劫持处理的,再对劫持的内存发起302跳转的回复,引导用户获取内容
  2. HTTP劫持: (访问谷歌但是⼀直有贪玩蓝月的⼴告),由于http明文传输,运营商会修改你的http响应内容(即加广告) 。最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商无法获取明文,就无法劫持你的响应内容。

# 浏览器内核

浏览器分为两部分:

  • shell:浏览器外壳,如菜单、工具栏等。
  • 内核:浏览器的核心,内核是基于标记语言显示内容的程序或模块。

浏览器内核主要分成两部分:

  1. 渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。默认情况下,渲染引擎可以显示 html、xml 文档及图片,它也可以借助插件显示其他类型数据,例如使用 PDF 阅读器插件,可以显示 PDF 格式。
  2. JS 引擎:解析和执行 javascript 来实现网页的动态效果。

最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。

# 常见浏览器内核

  • Trident(IE、百度):早期IE占有大量市场份额,所以很多网页按这个内核标准来写,但这个内核对真正的网页标准支持不是很好。微软长时间没有更新Trident内核,就导致内核与W3C标准脱节;且其内核大量Bug等安全问题没有解决,导致很多用户转向其他浏览器。
  • Gecko(Firefox、Flock):功能强大丰富,可以支持很多复杂网页效果和浏览器扩展接口。缺点:消耗资源多,如内存。
  • Presto(早期Opera):浏览网页速度最快的内核,处理JS脚本等脚本语言时比其他内核快3倍左右。缺点:为了速度而丢掉了一部分网页兼容性。
  • Webkit(Safari、中期Opera、早期Chrome):网页浏览速度较快。缺点:对于网页代码的容错性不高(兼容性较低),会使一些编写不标准的网页无法正确显示。
  • Blink(现在的Opera和Chrome):Blink引擎其实就是Webkit的一个分支。

IE+Chrome双内核:360、猎豹浏览器;Trident(兼容模式)+Webkit(高速模式):搜狗、QQ浏览器。

# 浏览器进程与线程

1603803289911-191cabf3-e7e2-4354-a83d-858668cc116f

Chrome浏览器包括1个浏览器主进程、1个GPU进程、1个网络进程、多个渲染进程和多个插件进程。

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • GPU进程:GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,现在独立出来成为一个单独的进程。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

# 浏览器渲染进程的线程

1603803289922-6462f252-aa33-4cf6-b541-d8bc2e285125

  1. GUI渲染线程:负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
  2. JS引擎线程:JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序,所以说JS是单线程执行的
  3. 事件触发线程:事件触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
  4. 定时器触发线程:即setIntervalsetTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中。
  5. 异步http请求线程:XMLHttpRequest连接后通过浏览器新开一个线程请求;检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行。

  1. GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  2. W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。

# 实现浏览器内多个标签页的通信

本质:通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。

  1. 使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。
  2. 使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。
  3. 使用 localStorage 的方式,我们可以在一个标签页对 localStorage 的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。
  4. 使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用 postMessage 方法,进行通信。

# Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

Service Worker 实现缓存功能一般分为三个步骤:

  1. 先注册 Service Worker;
  2. 监听到 install 事件以后就可以缓存需要的文件;
  3. 在下次用户访问的时候通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
    console.log('service worker 注册成功')
  })
    .catch(function(err) {
    console.log('servcie worker 注册失败')
  })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了:

1615478097248-5e3c9a93-5683-41de-8787-e87705d31fe1

在 Cache 中也可以发现所需的文件已被缓存:

1615477994804-e7eaa2c7-91bf-406d-b024-5add76d37d5e

# 浏览器渲染原理

  1. 浏览器先对得到的HTML进行解码,之后进行网络资源的预处理,将以后要发送的请求提前加进请求队列中。
  2. 浏览器将HTML转化为一个个的标记(标记化Tokenization),之后通过标记来构建DOM树;CSS同理,先进行标记化,再进行CSS样式树的构建。
  3. 浏览器将DOM树和CSS样式树结合,生成渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  4. 布局(回流/重排):浏览器根据渲染树,获取每个渲染对象在屏幕上的位置和尺寸。
  5. 绘制:遍历渲染树并调用渲染对象的 paint 方法将计算好的像素点绘制到屏幕。
  6. 渲染层合成:多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。

# 回流/重排

当渲染对象的位置,尺寸,或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程。

导致回流的操作:

  • 页面首次渲染
  • 元素位置或尺寸发生变化
  • 添加或删除可见的DOM元素
  • 浏览器窗口大小发生变化
  • 查询某些属性或调用某些方法
    • clientWidth、clientHeight、clientTop、clientLeft
    • offsetWidth、offsetHeight、offsetTop、offsetLeft
    • scrollWidth、scrollHeight、scrollTop、scrollLeft
    • scrollIntoView()、scrollIntoViewIfNeeded()
    • getComputedStyle():让浏览器更早地执行样式计算
    • getBoundingClientRect()
    • scrollTo()

# 重绘

样式的改变不改变渲染对象在文档流中的位置时浏览器重新绘制(如:color, background-color的改变)。

回流一定引发重绘,重绘不一定引发回流。回流比重绘的代价要更高。

# 减少回流与重绘

  • 渲染队列:浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。但当访问clientWidth, clientHeight之类的属性时,会刷新这个队列,所以要尽量减少这些属性的访问。
  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局。
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 将元素先设置display: none,DOM操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 对于复杂的动画效果,可以用绝对定位使其脱离文档流,这样他们发生变化就不会影响其他元素。
  • 使用CSS3中的transform, opacity, filters属性,启动GPU加速,这些属性的改变不会引发回流或重绘。

# 渲染过程遇到JS文件如何处理?

JavaScript的加载、解析与执行会阻塞HTML和CSS的解析。