如何防御Node.js中不安全的重定向

概念

对于Web开发人员来说,不安全或未经验证的重定向是一个必须要注意的地方。Express框架能够为重定向提供本地支持,使其易于实现和使用。但是,Express却将对输入进行验证的这项工作留给了开发者。
根据OWASP.org对“未经验证的重定向和转发”的定义,其概念如下:
未经验证的重定向和转发是指,Web应用程序接受不受信任的输入,而该输入可能导致Web应用程序将请求重定向到用户输入中包含的不受信任的URL。
通常,会在登录和身份验证的过程用到重定向,从而让用户在登陆之后重新回到登录前的页面。此外,还有其他方案能够实现这一点,具体根据业务需求或应用程序的类型而有所不同。

 

安全问题

如果不验证用户输入的重定向,那么攻击者就能够进行网络钓鱼诈骗,窃取用户凭据或执行其他恶意操作。
需要注意的是,当在Node.js或Express中实现重定向时,在服务器端进行输入验证非常重要。
如果攻击者发现某个站点没有验证外部用户提交的输入,那么攻击者可能会生成一个特制的链接,并在论坛、社交媒体或其他公共平台发布,以便诱导用户单击该链接。
从表面上看,这些URL的域名确实属于用户将要(或正在)访问的合法站点。例如:https://example.com/login?url=http://examp1e.com/bad/things ,确实是属于example.com 域名。
然而,如果服务器端的重定向逻辑没有对URL参数中的数据进行验证,那么用户最终访问到的网站可能是伪造的钓鱼站点(例如 examp1e.com )。
这只是攻击者如何利用不安全的重定向逻辑的一个简单示例。

 

不安全重定向的示例

在下面的代码中,我们看到/login接收来自URL参数的未经验证的数据,并且直接就将其传递给Express的res.redirect()方法。因此,只要用户通过身份验证,Express就会将用户重定向到输入或提供的任何URL。

var express = require('express');
var port = process.env.PORT || 3000;
var app = express();

app.get('/login', function (req, res, next) {

    if(req.session.isAuthenticated()) {

        res.redirect(req.query.url);
    }
}); 

app.get('/account', function (req, res, next) {
    res.send('Account page');
});

app.get('/profile', function (req, res, next) {
    res.send('Profile page');
});

app.listen(port, function() {
    console.log('Server listening on port ' + port);
});

 

防范方案1:输入验证

通常情况下,应该尽可能避免在代码中用到重定向和转发。
如果必须要在代码中使用重定向,那么首选的方案是使用事先定义的映射到特定目标的键。这种方案被称为白名单方法。具体实现方式如下:
1、baseHostname确保任何重定向都将不会离开我们的域名。
2、redirectMapping是一个对象,它将事先定义的键(例如传递给URL参数的内容)映射到服务器上的指定路径。
3、validateRedirect()方法判断事先定义的键是否存在。如果存在,就会返回重定向到相应路径。
4、修改/login逻辑,将baseHostname和redirectPath变量连接在一起,避免使用户提供的输入内容直接进入到Express的res.redirect()方法。
5、最后,使用encodeURI()方法作为额外的保证,确保串联字符串的URI部分被正确编码,只允许合法的重定向。
参考代码如下:

//Configure your whitelist
var baseHostname = "https://example.com";
var redirectMapping = {
    'account': '/account',
    'profile': '/profile'
}

//Create a function to validate whitelist
function validateRedirect(key) {
    if(key in redirectMapping) {

        return redirectMapping[key];
    }else{

        return false;
    }
}

app.get('/login', function (req, res, next) {
    if(req.session.isAuthenticated()) {
        redirectPath = validateRedirect(req.query.url);

        if(redirectPath) {
            res.redirect(encodeURI(baseHostname + redirectPath));
        }else{
            res.send('Not a valid redirect!');
        }
    }
});

 

防范方案2

在某些情况下,将每种合法的重定向列成白名单是不切实际的,但开发者仍然需要进行重定向,并且希望将重定向限制在域的范围内。这时,如果外部提供的值能够遵循特定模式(例如:都是16个字符的字符串,由字母和数字组成),可以采用这种方案。仅包含字母和数字的字符串是最为理想的,因为它们不包含任何可能有助于目录遍历、路径遍历等攻击的特殊字符(例如省略号、斜杠等)。
举例来说,开发人员可能希望用户在登陆后,能重定向回到电子商务网站上的特定项目。由于电子商务网站针对每种产品都有一个唯一的值,由字母和数字组成,因此开发者可以根据RegEx白名单验证外部输入,从而实现安全的重定向。在这种情况下,使用的是productId变量,如下面代码所示:

//配置白名单
var baseHostname = "https://example.com";

app.get('/login', function (req, res, next) {
    productId = (req.query.productId || '');
    whitelistRegEx = /^[a-zA-Z0-9]{16}$/;

    if(productId) {

        //Validate the productId is alphanumeric and exactly 16 characters
        if(whitelistRegEx.test(productId)) {

            res.redirect(encodeURI(baseHostname + '/item/' + productId));
        }else{

            //The productId did not meet the RegEx whitelist, so return an error
            res.send('Invalid product ID');
        }
    }else{

        //No productId was provided, so redirect to home page
        res.redirect('/');
    }
});

最后,警告用户他们正在被自动重定向是非常重要的。如果实际上需要将用户重定向到域名之外,那么可能需要在流程中创建一个中间页面,如下图所示,该页面能够提醒用户,并明确告知将要重定向到的URL。

(完)