5
1

More than 3 years have passed since last update.

node.js (Express) でのオープンリダイレクト脆弱性対策

Last updated at Posted at 2020-03-02

この記事は、WESEEK Tips Wiki に投稿された記事「/Tips/JavaScript/Express/オープンリダイレクト対策」の転載です。

オープンリダイレクトとは?

Express に於けるリダイレクト

Express 4.x API#res.redirect

  • URL パラメータ等、ユーザーが指定可能な文字列をそのままリダイレクトさせるコード書くとセキュリティホールとなる
  • res.redirect('//evil.example.com/path/to/attack') で再現可能
    • -> http://evil.example.com/path/to/attack にリダイレクトされる

過去実際にあった脆弱性

対策

方針

  1. URL をパースし、不正な形式(//evil.example.com 等)であればリダイレクトしない
  2. URL が、リクエストのホストと不一致ならリダイレクトしない
  3. URL が、ホワイトリストに入っていなければリダイレクトしない

コード

下記コードは、要求された URL へリダイレクトしない代わりに、サイトトップ(/)へのリダイレクトに振り替えている。

最小の対策(方針1,2まで)

middleware/safe-redirect.js
/**
 * Redirect with prevention from Open Redirect
 *
 * Usage: app.use(require('middleware/safe-redirect')())
 */
const logger = request('path/to/logger');


module.exports = () => {

  return function(req, res, next) {

    // extend res object
    res.safeRedirect = function(redirectTo) {
      if (redirectTo == null) {
        return res.redirect('/');
      }

      try {
        // check inner redirect
        const redirectUrl = new URL(redirectTo, `${req.protocol}://${req.get('host')}`);
        if (redirectUrl.hostname === req.hostname) {
          logger.debug(`Requested redirect URL (${redirectTo}) is local.`);
          return res.redirect(redirectUrl.href);
        }
        logger.debug(`Requested redirect URL (${redirectTo}) is NOT local.`);
      }
      catch (err) {
        logger.warn(`Requested redirect URL (${redirectTo}) is invalid.`, err);
      }

      logger.warn(`Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`);
      return res.redirect('/');
    };

    next();

  };

};

利用方法

app.use(require('middleware/safe-redirect')());

ホワイトリスト指定機能付き(方針3まで)

middleware/safe-redirect.js
/**
 * Redirect with prevention from Open Redirect
 *
 * Usage: app.use(require('middleware/safe-redirect')(['example.com', 'some.example.com:8080']))
 */
const logger = request('path/to/logger')('middleware:safe-redirect');


/**
 * Check whether the redirect url host is in specified whitelist
 * @param {Array<string>} whitelistOfHosts
 * @param {string} redirectToFqdn
 */
function isInWhitelist(whitelistOfHosts, redirectToFqdn) {
  if (whitelistOfHosts == null || whitelistOfHosts.length === 0) {
    return false;
  }

  const redirectUrl = new URL(redirectToFqdn);
  return whitelistOfHosts.includes(redirectUrl.hostname) || whitelistOfHosts.includes(redirectUrl.host);
}


module.exports = (whitelistOfHosts) => {

  return function(req, res, next) {

    // extend res object
    res.safeRedirect = function(redirectTo) {
      if (redirectTo == null) {
        return res.redirect('/');
      }

      try {
        // check inner redirect
        const redirectUrl = new URL(redirectTo, `${req.protocol}://${req.get('host')}`);
        if (redirectUrl.hostname === req.hostname) {
          logger.debug(`Requested redirect URL (${redirectTo}) is local.`);
          return res.redirect(redirectUrl.href);
        }
        logger.debug(`Requested redirect URL (${redirectTo}) is NOT local.`);

        // check whitelisted redirect
        const isWhitelisted = isInWhitelist(whitelistOfHosts, redirectTo);
        if (isWhitelisted) {
          logger.debug(`Requested redirect URL (${redirectTo}) is in whitelist.`, `whitelist=${whitelistOfHosts}`);
          return res.redirect(redirectTo);
        }
        logger.debug(`Requested redirect URL (${redirectTo}) is NOT in whitelist.`, `whitelist=${whitelistOfHosts}`);
      }
      catch (err) {
        logger.warn(`Requested redirect URL (${redirectTo}) is invalid.`, err);
      }

      logger.warn(`Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`);
      return res.redirect('/');
    };

    next();

  };

};
5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1