防止 nodejs 中的开放重定向攻击安全吗?

Is preventing open redirects attack in nodejs secure?

我正在尝试防止开放式重定向攻击。请查看下面的代码并检查安全性:

var = require('url');

// http://example.com/login?redirect=http://example.com/dashboard
app.route('/login', function (req, res, next) {
   var redirect = req.query.redirect,
        paths = url.parse(redirect); 

   if (paths.host !== req.headers.host) {
      return next(new Error('Open redirect attack detected'));
   }

   return res.redirect(redirect);
});

是否足以防止开放式重定向攻击,还是我应该添加其他内容?

CWE-601: URL Redirection to Untrusted Site ('Open Redirect')

Open Redirect的描述:

An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance.

防止开放重定向攻击的input validation策略建议:

Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a whitelist of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. Do not rely exclusively on looking for malicious or malformed inputs (i.e., do not rely on a blacklist). A blacklist is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, blacklists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. Use a whitelist of approved URLs or domains to be used for redirection.

使用 req.headers.hostreq.hostreq.hostname 是不安全的,因为 req.headers 可以被伪造(例如,一个 HTTP 请求有一个自定义的 Host header 访问用下面代码编写的 Express 应用程序)

var url = require('url');

app.get('/login', function (req, res, next) {
    var redirect = req.query.redirect,
        targetUrl = url.parse(redirect);
    console.log('req.headers.host: [%s]', req.headers.host);
    console.log('req.host: [%s]', req.host);
    console.log('req.hostname: [%s]', req.hostname);
    if (targetUrl.host != req.headers.host) {
        return next(new Error('Open redirect attack detected'));
    }
    return res.redirect(redirect);
});

使用curl发出请求:

$ curl -H 'Host: malicious.example.com' 'http://localhost:3012/login?redirect=http://malicious.example.com' -i
HTTP/1.1 302 Found
X-Powered-By: Express
Location: http://malicious.example.com
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 54
Date: Mon, 13 Jun 2016 06:30:55 GMT
Connection: keep-alive

$ #server output
req.headers.host: [malicious.example.com]
req.host: [malicious.example.com]
req.hostname: [malicious.example.com]

我建议你使用白名单来验证输入,示例代码如下:

const WHITELIST_TO_REDIRECT = new Set(["localhost:3012", "www.realdomain.com"]);

app.get('/login', function (req, res, next) {
   var redirect = req.query.redirect,
        targetUrl = url.parse(redirect);
   console.log("req.hostname: [%s]", req.hostname);
   console.log("url.host: [%s]", targetUrl.host);
   if (!WHITELIST_TO_REDIRECT.has(targetUrl.host)) {
      return next(new Error('Open redirect attack detected'));
   }

   return res.redirect(redirect);
});

在这种情况下,我会使用 HMAC。这将允许登录控制器验证 redirect 参数是由知道密钥的人生成的。

当您生成 "login" url 时,您将 redirect 参数的 HMAC 摘要与重定向参数本身一起添加到 url。

登录处理程序可以使用 HMAC 来确保 redirect 参数是由知道 HMAC 密钥的可信服务器生成的,从而防止开放重定向攻击。

例如

var crypto = require('crypto');
var secretKey = 'change-me';
var loginUrl = 'http://example.com/login'

// called to work out where to redirect to for login
function getLoginUrl(redirectBackUrl) {
    var sig = crypto.createHmac('sha1', secretKey)
                    .update(redirectBackUrl)
                    .digest('hex');

    return loginUrl 
         + '?redirect=' 
         + encodeURIComponent(redirectBackUrl) 
         +'&sig=' + sig;
}

// called by the login function to ensure that the 
// redirect parameter is valid
function isRedirectUrlValid(url, sig) {
    var expectedSig
            = crypto.createHmac('sha1', secretKey)
                    .update(url)
                    .digest('hex');

     return expectedSig === sig;
}