CSRF 跨站请求伪造防护问题以及如何修复它

什么是 CSRF?

CSRF 是跨站请求伪造(Cross-Site Request Forgery)  的首字母缩写。它通常是攻击者用来进入你的系统的一种攻击媒介。

你通常防止 CSRF 的方法是发送一个由每个 HTTP 请求产生的唯一的令牌(token)。如果服务器上的令牌与请求中的令牌不匹配,你会向用户显示一个错误。

CSRF 防护的标准

这是你用令牌对抗 CSRF 的一种方法:

const inital_token = '...';

const secure_fetch = (token => {
    const CSRF_HEADER = 'X-CSRF-TOKEN';
    return (url) => {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
              [CSRF_HEADER]: token
            }
        });
        response.then(res => {
           token = res.headers[CSRF_HEADER]
        });
        return response;
    };
})(inital_token);

这段代码使用 fetch API 来发送和接收 HTTP 报头中的安全令牌。在后端,你应该在页面加载时生成第一个初始令牌。在服务器上,在每个 AJAX请求中,你应该检查该令牌是否有效。

使用口令的问题

上述方法很有用,但是如果你打开一个以上的标签,它就有问题了。每个标签都可以向服务器发送请求,这将破坏这个解决方案。而高级用户可能无法以他们想要的方式使用你的应用程序。

但这个问题有一个简单的解决方案,即跨标签通信。

交叉标签通信解决方案

Sysend 库

你可以使用 Sysend 库,这是一个开源的解决方案,我专门为此目的而创建。它简化了跨标签的通信。

如果你愿意,你可以使用像 广播频道 这样的本地浏览器 API 来做同样的事情。在本文后面会有更多关于如何做到这一点的介绍。

但 Sysend 库对不支持 Broadcast Channel(广播频道)的浏览器也有作用。它也可以在 IE 中工作(它有一些错误,这并不奇怪)。你可能还需要支持一些旧的移动浏览器。它还有一个更简单的 API。

这是最简单的例子:

let token;
sysend.on('token', new_token => {
    token = new_token;
});

// ...

sysend.broadcast('token', token);

使用 sysend 库的基本功能的简单例子

而这就是你如何使用这个库来修复 CSRF 保护:

const inital_token = '...';

const secure_fetch = (token => {
    const CSRF_HEADER = 'X-CSRF-TOKEN';
    const EVENT_NAME = 'csrf';
    sysend.on(EVENT_NAME, new_token => {
        // get new toke from different tab
        token = new_token;
    });
    return (url) => {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
              [CSRF_HEADER]: token
            }
        });
        response.then(res => {
           token = res.headers[CSRF_HEADER];
           // send new toke to other tabs
           sysend.broadcast(EVENT_NAME, token); 
        });
        return response;
    };
})(inital_token);

使用 sysend 的带有 CSRF 保护的 secure_fetch 函数

你所要做的就是在发送请求时从其他标签页发送和接收一条信息。而你的 CSRF 保护的应用程序将在许多标签页上工作。

就这样了。这将让高级用户在想打开许多标签页时使用你的有 CSRF 保护的应用程序。

广播频道

下面是使用广播频道的最简单例子:

const channel = new BroadcastChannel('my-connection');
channel.addEventListener('message', (e) => {
    console.log(e.data); // 'some message'
});
channel.postMessage('some message');

广播频道的基本用法

因此,通过这个简单的 API,你可以做和上面一样的事情:

const inital_token = '...';

const secure_fetch = (token => {
    const CSRF_HEADER = 'X-CSRF-TOKEN';
    const channel = new BroadcastChannel('csrf-protection');
    channel.addEventListener('message', (e) => {
        // get new toke from different tab
     token = e.data;
    });
    return (url) => {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
              [CSRF_HEADER]: token
            }
        });
        response.then(res => {
           token = res.headers[CSRF_HEADER];
           // send new token to other tabs
           channel.postMessage(token);
        });
        return response;
    };
})(inital_token);

使用广播频道的带有 CSRF 保护的 secure_fetch 函数

正如你在上面的例子中看到的,广播频道没有任何事件命名空间。因此,如果你想发送多于一种类型的事件,你需要创建事件类型。

下面是一个使用广播频道的例子,除了我们到目前为止讨论的 CSRF 保护修复之外,还可以做更多的事情。

你可以为你的应用程序同步登录和注销。如果你登录到一个标签,你也会登录到其他标签。以同样的方式,你可以同步一些电子商务网站的购物车。

const channel = new BroadcastChannel('my-connection');
const CSRF = 'app/csrf';
const LOGIN = 'app/login';
const LOGOUT = 'app/logout';
let token;
channel.addEventListener('message', (e) => {
    switch (e.data.type) {
        case CSRF:
            token = e.data.payload;
            break;
        case LOGIN:
            const { user } = e.data.payload;
            autologin(user);
            break;
        case LOGOUT:
            logout();
            break;
    }
});

channel.postMessage({type: 'login', payload: { user } });

使用具有不同类型信息的广播频道

总结

如果你保护你的应用免受攻击,那就太好了。 但是请想清楚人们将如何使用你的应用程序,避免不必要的东西,导致难以使用。这不仅适用于这个特定问题。

Sysend 库是一种在同一浏览器中打开的选项卡之间进行通信的简单方法。 它可以解决 CSRF 保护的主要问题。 该库具有更多功能,你可以查看其 GitHub repo 了解更多详细信息。

广播频道也没有那么复杂。 如果你不需要支持旧的浏览器或一些旧的移动设备,你可以使用这个 API。 但是如果你需要支持旧的浏览器,或者想让你的代码更简单,你可以使用 sysend 库。