跨站请求伪造 CSRF

跨站请求伪造 CSRF(Cross-site request forgery)

挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

例子

假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName 那么,一个恶意攻击者可以在另一个网站上放置如下代码:

<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">

如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。 这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>我的银行</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.css">
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <p>用户名
                            <span id="username"></span>
                        </p>
                        <p>余额
                            <span id="money"></span>
                        </p>
                    </div>
                    <div class="panel-body">
                        <form onsubmit="transfer(event)">
                            <div class="form-group">
                                <label for="target">转账用户</label>
                                <input id="target" class="form-control" placeholder="请输入的用户名">
                            </div>
                            <div class="form-group">
                                <label for="amount">金额</label>
                                <input id="amount" class="form-control" placeholder="请输入转账的金额">
                            </div>
                            <div class="form-group">
                                <input type="submit" class="btn btn-primary">
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script>
        $(function () {
            $.get('/api/user').then(data => {
                console.log(data);
                console.log(data.user.username);
                if (data.code == 0) {
                    $('#username').html(data.user.username);
                    $('#money').html(data.user.money);
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
        });
        function transfer(event) {
            event.preventDefault();
            let target = $('#target').val();
            let amount = $('#amount').val();
            $.post('/api/transfer', { target, amount }).then(data => {
                if (data.code == 0) {
                    alert('转账成功');
                    location.reload();
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
        }
    </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>我的银行</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.css">
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <p>用户名
                            <span id="username"></span>
                        </p>
                        <p>余额
                            <span id="money"></span>
                        </p>
                    </div>
                    <div class="panel-body">
                        <form onsubmit="transfer(event)">
                            <div class="form-group">
                                <label for="target">转账用户</label>
                                <input id="target" class="form-control" placeholder="请输入的用户名">
                            </div>
                            <div class="form-group">
                                <label for="amount">金额</label>
                                <input id="amount" class="form-control" placeholder="请输入转账的金额">
                            </div>
                            <div class="form-group">
                                <input type="submit" class="btn btn-primary">
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script>
        $(function () {
            $.get('/api/user').then(data => {
                console.log(data);
                console.log(data.user.username);
                if (data.code == 0) {
                    $('#username').html(data.user.username);
                    $('#money').html(data.user.money);
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
        });
        function transfer(event) {
            event.preventDefault();
            let target = $('#target').val();
            let amount = $('#amount').val();
            $.post('/api/transfer', { target, amount }).then(data => {
                if (data.code == 0) {
                    alert('转账成功');
                    location.reload();
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
        }
    </script>
</body>

</html>
app.get('/api/user', function (req, res) {
    let { username } = userSessions[req.cookies.sessionId];
    if (username) {
        let user;
        for (let i = 0; i < users.length; i++) {
            if (username == users[i].username) {
                user = users[i];
                break;
            }
        }
        res.json({ code: 0, user });
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
});

app.post('/api/transfer', function (req, res) {
    let { target, amount } = req.body;
    amount = isNaN(amount) ? 0 : Number(amount);
    let { username } = userSessions[req.cookies.sessionId];
    if (username) {
        let user;
        for (let i = 0; i < users.length; i++) {
            if (username == users[i].username) {
                users[i].money -= amount;
            } else if (target == users[i].username) {
                users[i].money += amount;
            }
        }
        res.json({ code: 0 });
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
})

app.get('/api/user', function (req, res) {
    let { username } = userSessions[req.cookies.sessionId];
    if (username) {
        let user;
        for (let i = 0; i < users.length; i++) {
            if (username == users[i].username) {
                user = users[i];
                break;
            }
        }
        res.json({ code: 0, user });
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
});

app.post('/api/transfer', function (req, res) {
    let { target, amount } = req.body;
    amount = isNaN(amount) ? 0 : Number(amount);
    let { username } = userSessions[req.cookies.sessionId];
    if (username) {
        let user;
        for (let i = 0; i < users.length; i++) {
            if (username == users[i].username) {
                users[i].money -= amount;
            } else if (target == users[i].username) {
                users[i].money += amount;
            }
        }
        res.json({ code: 0 });
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
})

防御措施

检查Referer字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。

这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。


function getClientToken() {
            let result = document.cookie.match(/token=([^;]+)/);
            return result ? result[1] : '';
        }
function transfer(event) {
            event.preventDefault();
            let target = $('#target').val();
            let amount = $('#amount').val();
            let captcha = $('#captcha').val();
            $.post('/api/transfer', {
                target,
                amount,
                captcha,
                clientToken: getClientToken()
            }).then(data => {
                if (data.code == 0) {
                    alert('转账成功');
                    location.reload();
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
}


function getClientToken() {
            let result = document.cookie.match(/token=([^;]+)/);
            return result ? result[1] : '';
        }
function transfer(event) {
            event.preventDefault();
            let target = $('#target').val();
            let amount = $('#amount').val();
            let captcha = $('#captcha').val();
            $.post('/api/transfer', {
                target,
                amount,
                captcha,
                clientToken: getClientToken()
            }).then(data => {
                if (data.code == 0) {
                    alert('转账成功');
                    location.reload();
                } else {
                    alert('用户未登录');
                    location.href = '/login.html';
                }
            });
}

验证码

通过设置验证码来提高安全措施


var svgCaptcha = require('svg-captcha');
app.get('/api/captcha', function (req, res) {
    let session = userSessions[req.cookies.sessionId];
    if (session) {
        var codeConfig = {
            size: 5,// 验证码长度
            ignoreChars: '0o1i', // 验证码字符中排除 0o1i
            noise: 2, // 干扰线条的数量
            height: 44
        }
        var captcha = svgCaptcha.create(codeConfig);
        session.captcha = captcha.text.toLowerCase(); //存session用于验证接口获取文字码
        res.send({ code: 0, captcha: captcha.data });
    } else {
        res.json({ code: 1, data: '没有该用户' });
    }
});

var svgCaptcha = require('svg-captcha');
app.get('/api/captcha', function (req, res) {
    let session = userSessions[req.cookies.sessionId];
    if (session) {
        var codeConfig = {
            size: 5,// 验证码长度
            ignoreChars: '0o1i', // 验证码字符中排除 0o1i
            noise: 2, // 干扰线条的数量
            height: 44
        }
        var captcha = svgCaptcha.create(codeConfig);
        session.captcha = captcha.text.toLowerCase(); //存session用于验证接口获取文字码
        res.send({ code: 0, captcha: captcha.data });
    } else {
        res.json({ code: 1, data: '没有该用户' });
    }
});

bank.html

<div class="form-group">
    <label for="captcha" id="captcha"></label>
    <input id="captcha" class="form-control" placeholder="请输入验证码">
</div>


 $.get('/api/captcha').then(data => {
    if (data.code == 0) {
        $('#captcha').html(data.captcha);
    } else {
        alert('用户未登录');
        location.href = '/login.html';
    }
});
<div class="form-group">
    <label for="captcha" id="captcha"></label>
    <input id="captcha" class="form-control" placeholder="请输入验证码">
</div>


 $.get('/api/captcha').then(data => {
    if (data.code == 0) {
        $('#captcha').html(data.captcha);
    } else {
        alert('用户未登录');
        location.href = '/login.html';
    }
});

referer验证

let referer = req.headers['referer'];
   if (/^https?:\/\/localhost:3000/.test(referer)) {

   } else {
       res.json({ code: 1, error: 'referer不正确' });
   }
let referer = req.headers['referer'];
   if (/^https?:\/\/localhost:3000/.test(referer)) {

   } else {
       res.json({ code: 1, error: 'referer不正确' });
   }

server.js

app.post('/api/transfer', function (req, res) {
    // let referer = req.headers['referer'];
    //if (/^https?:\/\/localhost:3000/.test(referer)) {
    let { target, amount, clientToken, captcha } = req.body;
    amount = isNaN(amount) ? 0 : Number(amount);
    let { username, token } = userSessions[req.cookies.sessionId];
    if (username) {
        if (clientToken == token) {
            let user;
            for (let i = 0; i < users.length; i++) {
                if (username == users[i].username) {
                    users[i].money -= amount;
                } else if (target == users[i].username) {
                    users[i].money += amount;
                }
            }
            res.json({ code: 0 });
        } else {
            res.json({ code: 1, error: '违法操作' });
        }
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
    //} else {
    res.json({ code: 1, error: 'referer不正确' });
    //}
})

app.post('/api/transfer', function (req, res) {
    // let referer = req.headers['referer'];
    //if (/^https?:\/\/localhost:3000/.test(referer)) {
    let { target, amount, clientToken, captcha } = req.body;
    amount = isNaN(amount) ? 0 : Number(amount);
    let { username, token } = userSessions[req.cookies.sessionId];
    if (username) {
        if (clientToken == token) {
            let user;
            for (let i = 0; i < users.length; i++) {
                if (username == users[i].username) {
                    users[i].money -= amount;
                } else if (target == users[i].username) {
                    users[i].money += amount;
                }
            }
            res.json({ code: 0 });
        } else {
            res.json({ code: 1, error: '违法操作' });
        }
    } else {
        res.json({ code: 1, error: '用户没有登录' });
    }
    //} else {
    res.json({ code: 1, error: 'referer不正确' });
    //}
})