CSRF 攻击与防范

跨站请求伪造

跨站请求伪造 ( Cross Site Request Forgery, 缩写 CSRF ),是一种网络攻击方式。

原理上,它欺骗服务器,让服务器误认为攻击请求是用户自己发出的合法请求,以达到攻击的目的。

攻击的原理

  1. 用户登陆 站点A。
  2. 没有注销 站点A 的情况下,访问包含攻击代码的 站点B。
  3. 站点B 向 站点A 发送包含敏感操作的请求,这时候浏览器会带上 站点A 的 cookie。
  4. 站点A 的服务器收到请求,通过 cookie 识别出了用户,开始执行请求的敏感操作。

我们可以发现,这里面的核心思想是,服务器没有或不能分辨包含敏感操作的请求是否是用户自己发出的

防御攻击

既然明白了攻击的原理,那么就可以针对性的作出防御。核心在于,服务器需要甄别请求是否是用户自身发出的。

实践上有下面几种常见的防御手段。

READ MORE...

阻塞、非阻塞IO,同步、异步IO

今天闲暇看 Node.js,看到这些概念,有些困惑的地方,Google 一番,做些笔记。 因为术业方向问题,可能理解有误,但记录下来便于日后复习的时候比对。

阻塞 IO (Blocking IO), 非阻塞 IO (Non-Blocking IO)

核心区别在于,发起调用的进程(或线程)是否会挂起等待 IO 完成。

下面举例说明用户进程发起 一个 read 数据的阻塞 IO 操作的过程。 进程在发起系统调用后,就会一直干等。

  1. 首先等待 kernel 准备数据(如等待完整的网络数据包),这个阶段是 Blocking 的。
  2. 数据准备完毕后,kernel 将数据从缓冲区拷贝到用户空间,这个阶段也是 Blocking 的。
  3. 调用返回,用户进程也就解除了阻塞状态。

由上述过程可见,阻塞 IO 操作的流程很简单易懂,但也可以遇见,如果 IO 操作非常费时,那么用户进程在这个过程将会无法响应其他操作,这对某些应用来说,是无法接受的。

那么,一个非阻塞的 IO 又是怎么样的呢?

  1. 一个read的非阻塞IO,不会挂起用户进程,在 kernel 准备数据中的时候,收到系统调用后,会立即返回一个错误状态,用户进程不会被挂起,是Non-Blocking的。
  2. 发起调用的进程因为马上就能获得数据的准备的状态,因此可以周期性继续发起调用,直至 kernel 准备好数据。
  3. 当 kernel 准备好数据后,再接收到读取的调用的话,就会将数据拷贝到用户空间,这个阶段是Blocking的。
  4. 调用返回,圆满结束。

由上述过程可见,非阻塞 IO 操作的流程相比复杂一些,但因为用户进程不会被挂起,因此有机会在 IO 过程做其他事情,灵活很多。

同步 IO

通常情况下, 不论阻塞 IO 还是非阻塞 IO, 均是同步 IO。

异步 IO

着重说明下异步 IO, 因为这是我们 JavaScript 开发者接触最多的一个概念。

一个异步的 IO,发起调用户后会立即返回,可以不必等待 IO 结果去做其他事情,当有结果之后,会收到一个信号通知,从而可以回头进入数据处理环节。

具体的流程如下:

  1. 发起异步 IO,立即返回,可以做其他事情
  2. kernel 会默默地准备数据、拷贝数据到内存用户空间,向用户进程发一个 signal 通知其IO已完成
  3. 调用发起者处理 IO 结果

由此可见,一个异步的 IO 全程不阻塞,也不必关心 IO 的过程状态,省心省事。

READ MORE...

IIFE(立即执行函数)

在 JavaScript 中,IIFE 被广泛应用。

比如,我们经常能在一些 JavaScript 项目中,看到所有代码被包裹在一个 IIFE 中

1
2
3
(function(){
  // 项目实现代码
}());

IIFE 的使用理由

使用 IIFE 的一个重要原因是,可以避免对外层作用域的污染。

因为IIFE的本质就是一个函数,我们在函数内部定义的所有变量、函数,均在函数本地生效,避免了跟其他 JavaScript 冲突。

声明变量务必使用 var(ES6 之后支持 let,const)。 因为非 strict mode 中,不使用 var 声明的变量,实际上是定义在全局作用域的。

此外还有一些有用的特性也是使用 IIFE 的理由之一,例如提供正确的 undefined 值:

1
2
3
4
5
6
(function(undefined){
  // undefined 可能被重新赋值
  // 无传递实参,确保了
  // 形参 `undefined` 是
  // 真正的 `undefined`
}());

当然利用这种模式,也可以往 IIFE 里面注入一些其他的依赖,比如全局对象等等。

IIFE 的执行原理

奥妙就在于 () 上面,因为()运算符内部必须是一个表达式。 所以,() 内部的函数不是一个函数声明,而是一个函数表达式,表达式的计算结果返回一个函数,然后立即被调用。

这跟下面的函数表达式一样道理(= 运算符右侧接受一个表达式):

1
2
3
4
var exprVal = function() {
  return 'haha~'
}()
// exprVal === 'haha~'

IIFE 的其他写法

既然明白了 IIFE 的原理就是利用的函数表达式的性质,那么我们就可以有很多种写法了,例如

1
2
3
4
(function(){ /* 代码 */ })();
!function(){ /* 代码 */ }();
+function(){ /* 代码 */ }();
void function(){ /* 代码 */ }();

选择哪种写法基本取决于个人审美,不过惯例上使用括号的比较多。

但,不同的写法仍有一些其他的细微影响。

比如分号的使用,比如括号的方式,因为括号的多义性,如果前面还有内容,那么括号会被当作是一个函数调用,导致非期望的结果出现。

一个好的实践方式是,在括号、中括号等前面随手加上分号:

1
;(function(){ /* 代码 */ }())

这样可以避免 IIFE 前面的代码忘记加分号导致的问题。

READ MORE...

DOM 节点集合及其活动(live)、静态(static)之分

我们批量处理页面上元素的时候,需要先选择出对应的节点集合,常用的有以下 API

  • document.getElementsByClassName()
    • 返回一个 HTMLCollection,且是活动的
  • document.getElementsByTagName()
    • 返回一个 HTMLCollection,且活动的
  • document.getElementsByName()
    • 返回一个 NodeList,且是活动的
  • document.querySelectorAll()
    • 返回一个 NodeList,且是静态的

上面出现了两种 collection,一种是 HTMLCollection ,一种是 NodeList。 那么这两种集合有什么区别呢?

NodeList

一个 NodeList 是一组节点(nodes)的集合。

HTMLCollection

一个 HTMLCollection 是一组元素(elements)的集合。 有个 nameItem 方法是 NodeList 中没有的。

HTMLCollection 是一个历史产物,标准设计新 API 不会再使用这个接口了。

说完两种集合,再来解释 live、static 的概念

活动的(live)、静态的(static)

一个活动的集合,意味着在文档改变的时候,会自动更新这个集合。 而一个静态的集合,则只是生成了一个选择时的快照( snapshot ),不会自动刷新。

看代码:

HTML:

1
2
3
4
5
<!-- 省略前面 -->
<body>
  <p><p>
</body>
<!-- 省略后面 -->

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
var static = document.querySelectorAll('p') 
var live = document.getElementsByTagName('p')

// 集合初始都包含一个`p`
console.log(static.length) // 1
console.log(live.length) // 1

// 往`body`里插入一个 `p`
document.body.appendChild(document.createElement('p'))

// 活动的集合会自动刷新,静态的集合不变
console.log(static.length) // 1
console.log(live.length) // 2  

live 特性可能带来的问题

因为文档的改变会反应到活动的集合上,所以我们在循环处理一个活动的集合的时候,必须非常小心,不要在循环体上插入符合选择条件的元素,否则可能出现死循环。

例子代码:

1
2
3
4
5
var live = document.getElementsByTagName('p')
// 死循环
for (var i = 0; i < live.length; i += 1) {
  document.body.appendChild(document.createElement('p'))
}
READ MORE...

JavaScript 作用域

作用域的概念

作用域是一个抽象概念,它描述了一组 binding (绑定) 的生效范围。

我们的程序中的某个部分,某个 name 绑定着某个 entity,而同样的 name,在我们程序的另一个部分,可能绑定了另一个 entity,或者根本不存在这个 name

这段话用了几个抽象概念描述作用域的表现形式。即,作用域就是一组 name 跟 entity 的绑定,这种绑定是有起作用的范围的。

解释一下这里用到的几个概念:

  • name: 名称,可理解为允许我们引用变量、常量、函数、对象等等等的标识符。
  • entity: 实体,可理解为内存中实际的数据、代码等
  • binding: 绑定,即 name 跟 entity 之间的关联关系。一个 name 绑定一个 entity,称为引用该对象。

在 JavaScript 中,两个不同的函数内部的同名变量,就完全是不同的东西,有各自的作用范围。 而作用域,正是这种限定变量在程序不同部分的可访问性的概念。

READ MORE...