本周面试题一览:javascript
XSS(Cross-Site Scripting,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登录网站时就会执行这些恶意代码,这些脚本能够读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。css
XSS 的本质是:恶意代码未通过滤,与网站正常的代码混在一块儿;浏览器没法分辨哪些脚本是可信的,致使恶意脚本被执行。因为直接在用户的终端执行,恶意代码可以直接获取用户的信息,利用这些信息冒充用户向网站发起攻击者定义的请求。html
根据攻击的来源,XSS攻击能够分为存储型(持久性)、反射型(非持久型)和DOM型三种。下面咱们来详细了解一下这三种XSS攻击:前端
1.1 反射型XSS
当用户点击一个恶意连接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,好比一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。java
反射型 XSS 的攻击步骤:git
URL
,其中包含恶意代码。URL
时,网站服务端将恶意代码从 URL
中取出,拼接在 HTML 中返回给浏览器。反射型 XSS 漏洞常见于经过 URL
传递参数的功能,如网站搜索、跳转等。因为须要用户主动打开恶意的 URL
才能生效,攻击者每每会结合多种手段诱导用户点击。github
POST 的内容也能够触发反射型 XSS,只不过其触发条件比较苛刻(须要构造表单提交页面,并引导用户点击),因此很是少见。web
若是不但愿被前端拿到cookie,后端能够设置 httpOnly
(不过这不是 XSS攻击
的解决方案,只能下降受损范围)面试
如何防范反射型XSS攻击数据库
对字符串进行编码。
对url的查询参数进行转义后再输出到页面。
app.get('/welcome', function(req, res) { //对查询参数进行编码,避免反射型 XSS攻击 res.send(`${encodeURIComponent(req.query.type)}`); });
1.2 DOM 型 XSS
DOM 型 XSS 攻击,实际上就是前端 JavaScript
代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML
、.outerHTML
、.appendChild
、document.write()
等API时要特别当心,不要把不可信的数据做为 HTML 插到页面上,尽可能使用 .innerText
、.textContent
、.setAttribute()
等。
DOM 型 XSS 的攻击步骤:
如何防范 DOM 型 XSS 攻击
防范 DOM 型 XSS 攻击的核心就是对输入内容进行转义(DOM 中的内联事件监听器和连接跳转都能把字符串做为代码运行,须要对其内容进行检查)。
1.对于url
连接(例如图片的src
属性),那么直接使用 encodeURIComponent
来转义。
2.非url
,咱们能够这样进行编码:
function encodeHtml(str) { return str.replace(/"/g, '"') .replace(/'/g, ''') .replace(/</g, '<') .replace(/>/g, '>'); }
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。
1.3 存储型XSS
恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的缘由仍然是没有作好数据过滤:前端提交数据至服务端时,没有作好过滤;服务端在接受到数据时,在存储以前,没有作过滤;前端从服务端请求到数据,没有过滤输出。
存储型 XSS 的攻击步骤:
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
如何防范存储型XSS攻击:
除了谨慎的转义,咱们还须要其余一些手段来防范XSS攻击:
1.Content Security Policy
在服务端使用 HTTP的 Content-Security-Policy
头部来指定策略,或者在前端设置 meta
标签。
例以下面的配置只容许加载同域下的资源:
Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">
前端和服务端设置 CSP 的效果相同,可是meta
没法使用report
严格的 CSP 在 XSS 的防范中能够起到如下的做用:
2.输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然没法彻底防止 XSS 发生,但能够增长 XSS 攻击的难度。
3.输入内容限制
对于部分输入,能够限定不能包含特殊字符或者仅能输入数字等。
4.其余安全措施
屏幕并非惟一的输出机制,好比说屏幕上看不见的元素(隐藏的元素),其中一些依然可以被读屏软件阅读出来(由于读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,咱们将其归为三大类:
display
属性(不占据空间)display: none;
HTML5 新增属性,至关于 display: none
<div hidden> </div>
position
和 盒模型 将元素移出可视区范围posoition
为 absolute
或 fixed
,经过设置 top
、left
等值,将其移出可视区域。(可视区域不占位)position:absolute; left: -99999px;
position
为 relative
,经过设置 top
、left
等值,将其移出可视区域。(可视区域占位)position: relative; left: -99999px;
如但愿其在可视区域不占位置,需同时设置 height: 0
;
margin-left: -99999px;
若是但愿其在可视区域不占位,需同时设置 height: 0
;
transform: scale(0);
若是但愿不占据空间,需同时设置 height: 0
translateX
, translateY
(占据空间)transform: translateX(-99999px);
若是但愿不占据空间,需同时设置 height: 0
rotate
(占据空间)transform: rotateY(90deg);
宽高为0,字体大小为0:
height: 0; width: 0; font-size: 0;
宽高为0,超出隐藏:
height: 0; width: 0; overflow: hidden;
opacity: 0;
visibility
属性 (占据空间)visibility: hidden
z-index
属性 (占据空间)position: relative; z-index: -999;
再设置一个层级较高的元素覆盖在此元素上。
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
读屏软件不可读,占据空间,可见。
<div aria-hidden="true"> </div>
在说浏览器事件代理机制原理以前,咱们首先了解一下事件流的概念,早期浏览器,IE采用的是事件捕获事件流,而Netscape采用的则是事件冒泡。"DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。现代浏览器也都遵循此规范。
事件代理又称为事件委托,在祖先级 DOM 元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件冒泡的原理来触发绑定在祖先级 DOM 的事件。由于事件会从目标元素一层层冒泡至 document 对象。
添加到页面上的事件数量会影响页面的运行性能,若是添加的事件过多,会致使网页的性能降低。采用事件代理的方式,能够大大减小注册事件的个数。
事件代理的当时,某个子孙元素是动态增长的,不须要再次对其进行事件绑定。
不用担忧某个注册了事件的DOM元素被移除后,可能没法回收其事件处理程序,咱们只要把事件处理程序委托给更高层级的元素,就能够避免此问题。
容许给一个事件注册多个监听。
提供了一种更精细的手段控制 listener
的触发阶段(能够选择捕获或者是冒泡)。
对任何 DOM
元素都是有效的,而不只仅是对 HTML
元素有效。
addEventListener 接受3个参数,分别是要处理的事件名、实现了 EventListener 接口的对象或者是一个函数、一个对象/一个布尔值。
target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
options(对象) | 可选
capture: Boolean
。true 表示在捕获阶段触发,false表示在冒泡阶段触发。默认是 false。
once:Boolean
。true 表示listener 在添加以后最多只调用一次,listener 会在其被调用以后自动移除。默认是 false。
passive: Boolean
。true 表示 listener 永远不会调用 preventDefault()
。若是 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。默认是 false。
useCapture(Boolean) | 可选
useCapture
默认为 false。表示冒泡阶段调用事件处理程序,若设置为 true,表示在捕获阶段调用事件处理程序。
如将页面中的全部click事件都代理到document上:
document.addEventListener('click', function (e) { console.log(e.target); /** * 捕获阶段调用调用事件处理程序,eventPhase是 1; * 处于目标,eventPhase是2 * 冒泡阶段调用事件处理程序,eventPhase是 3; */ console.log(e.eventPhase); }, false);
与 addEventListener
相对应的是 removeEventListener
,用于移除事件监听。
setTimeout
只能保证延时或间隔不小于设定的时间。由于它实际上只是将回调添加到了宏任务队列中,可是若是主线程上有任务尚未执行完成,它必需要等待。
若是你对前面这句话不是很是理解,那么有必要了解一下 JS的运行机制。
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。
(2)主线程以外,还存在"任务队列"(task queue)。
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
如 setTimeout(()=>{callback();}, 1000)
,即表示在1s以后将 callback
放到宏任务队列中,当1s的时间到达时,若是主线程上有其它任务在执行,那么 callback
就必需要等待,另外 callback
的执行也须要时间,所以 setTimeout
的时间间隔是有偏差的,它只能保证延时不小于设置的时间。
setTimeout
的偏差咱们只能减小执行屡次的 setTimeout
的偏差,例如倒计时功能。
倒计时的时间一般都是从服务端获取的。形成偏差的缘由:
1.没有考虑偏差时间(函数执行的时间/其它代码的阻塞)
2.没有考虑浏览器的“休眠”
彻底消除 setTimeout
的偏差是不可能的,可是咱们减小 setTimeout
的偏差。经过对下一次任务的调用时间进行修正,来减小偏差。
let count = 0; let countdown = 5000; //服务器返回的倒计时时间 let interval = 1000; let startTime = new Date().getTime(); let timer = setTimeout(countDownStart, interval); //首次执行 //定时器测试 function countDownStart() { count++; const offset = new Date().getTime() - (startTime + count * 1000); const nextInterval = interval - offset; //修正后的延时时间 if (nextInterval < 0) { nextInterval = 0; } countdown -= interval; console.log("偏差:" + offset + "ms,下一次执行:" + nextInterval + "ms后,离活动开始还有:" + countdown + "ms"); if (countdown <= 0) { clearTimeout(timer); } else { timer = setTimeout(countDownStart, nextInterval); } }
若是当前页面是不可见的,那么倒计时会出现大于100ms的偏差时间。所以在页面显示时,应该从新从服务端获取剩余时间进行倒计时。固然,为了更好的性能,当倒计时不可见(Tab页切换/倒计时内容不在可视区时),能够选择中止倒计时。
为此,咱们能够监听 visibityChange
事件进行处理。