この記事は、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')())
*/constlogger=request('path/to/logger');module.exports=()=>{returnfunction(req,res,next){// extend res objectres.safeRedirect=function(redirectTo){if(redirectTo==null){returnres.redirect('/');}try{// check inner redirectconstredirectUrl=newURL(redirectTo,`${req.protocol}://${req.get('host')}`);if(redirectUrl.hostname===req.hostname){logger.debug(`Requested redirect URL (${redirectTo}) is local.`);returnres.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.`);returnres.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']))
*/constlogger=request('path/to/logger')('middleware:safe-redirect');/**
* Check whether the redirect url host is in specified whitelist
* @param {Array<string>} whitelistOfHosts
* @param {string} redirectToFqdn
*/functionisInWhitelist(whitelistOfHosts,redirectToFqdn){if(whitelistOfHosts==null||whitelistOfHosts.length===0){returnfalse;}constredirectUrl=newURL(redirectToFqdn);returnwhitelistOfHosts.includes(redirectUrl.hostname)||whitelistOfHosts.includes(redirectUrl.host);}module.exports=(whitelistOfHosts)=>{returnfunction(req,res,next){// extend res objectres.safeRedirect=function(redirectTo){if(redirectTo==null){returnres.redirect('/');}try{// check inner redirectconstredirectUrl=newURL(redirectTo,`${req.protocol}://${req.get('host')}`);if(redirectUrl.hostname===req.hostname){logger.debug(`Requested redirect URL (${redirectTo}) is local.`);returnres.redirect(redirectUrl.href);}logger.debug(`Requested redirect URL (${redirectTo}) is NOT local.`);// check whitelisted redirectconstisWhitelisted=isInWhitelist(whitelistOfHosts,redirectTo);if(isWhitelisted){logger.debug(`Requested redirect URL (${redirectTo}) is in whitelist.`,`whitelist=${whitelistOfHosts}`);returnres.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.`);returnres.redirect('/');};next();};};