オープンリダイレクトとは?
[/用語集/オープンリダイレクト]
Express に於けるリダイレクト
Express 4.x API#res.redirect external_link
- URL パラメータ等、ユーザーが指定可能な文字列をそのままリダイレクトさせるコード書くとセキュリティホールとなる
過去実際にあった脆弱性
- GROWI v3.4.6 以前にオープンリダイレクトの脆弱性が含まれていた
res.redirect('//evil.example.com/path/to/attack')
で再現可能- ->
http://evil.example.com/path/to/attack
にリダイレクトされる
- ->
対策
方針
- URL をパースし、不正な形式(
//evil.example.com
等)であればリダイレクトしない - 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(); }; };