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

[/用語集/オープンリダイレクト]

# Express に於けるリダイレクト

[Express 4.x API#res.redirect](http://expressjs.com/en/api.html#res.redirect)

- URL パラメータ等、ユーザーが指定可能な文字列をそのままリダイレクトさせるコード書くとセキュリティホールとなる

# 過去実際にあった脆弱性

- GROWI v3.4.6 以前にオープンリダイレクトの脆弱性が含まれていた
    - [GROWI 脆弱性対応のお知らせ (JVN#84876282) ](https://weseek.co.jp/security/2019/06/04/growi-fix-jvn84876282/)
- `res.redirect('//evil.example.com/path/to/attack')` で再現可能
    - -> `http://evil.example.com/path/to/attack` にリダイレクトされる

# 対策

## 方針

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

## コード

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

```javascript: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();

  };

};
```

#### 利用方法

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

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

```javascript: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();

  };

};
```

---

## Page Navigation

- Canonical URL: https://tips.weseek.co.jp/Tips/JavaScript/Express/オープンリダイレクト対策
- Permalink: https://tips.weseek.co.jp/5e5d51463051ce00499cac59
- Parent: [Express](/65069da24603e7ff39a6b3db.md)
- Children: 0 total
- Total descendants: 0
- Siblings: 0 total
- Last updated: 2020-03-02T18:34:33.033Z by yuki
- Full page listing (all children regardless of count): https://tips.weseek.co.jp/_api/v3/page-listing/children?id=5e5d51463051ce00499cac59
