この記事は、WESEEK Tips Wiki に投稿された記事「/Tips/JavaScript/Express/オープンリダイレクト対策」の転載です。
オープンリダイレクトとは?
Express に於けるリダイレクト
- URL パラメータ等、ユーザーが指定可能な文字列をそのままリダイレクトさせるコード書くとセキュリティホールとなる
-
res.redirect('//evil.example.com/path/to/attack')
で再現可能- ->
http://evil.example.com/path/to/attack
にリダイレクトされる
- ->
過去実際にあった脆弱性
- GROWI v3.4.6 以前にオープンリダイレクトの脆弱性が含まれていた
対策
方針
- URL をパースし、不正な形式(
//evil.example.com
等)であればリダイレクトしない - URL が、リクエストのホストと不一致ならリダイレクトしない
- 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();
};
};