如何防禦Node.js中不安全的重定向
概念
對於Web開發人員來說,不安全或未經驗證的重定向是一個必須要注意的地方。Express框架能夠為重定向提供本地支援,使其易於實現和使用。但是,Express卻將對輸入進行驗證的這項工作留給了開發者。
根據OWASP.org對“未經驗證的重定向和轉發”的定義,其概念如下:
未經驗證的重定向和轉發是指,Web應用程式接受不受信任的輸入,而該輸入可能導致Web應用程式將請求重定向到使用者輸入中包含的不受信任的URL。
通常,會在登入和身份驗證的過程用到重定向,從而讓使用者在登陸之後重新回到登入前的頁面。此外,還有其他方案能夠實現這一點,具體根據業務需求或應用程式的型別而有所不同。
安全問題
如果不驗證使用者輸入的重定向,那麼攻擊者就能夠進行網路釣魚詐騙,竊取使用者憑據或執行其他惡意操作。
需要注意的是,當在Node.js或Express中實現重定向時,在伺服器端進行輸入驗證非常重要。
如果攻擊者發現某個站點沒有驗證外部使用者提交的輸入,那麼攻擊者可能會生成一個特製的連結,並在論壇、社交媒體或其他公共平臺釋出,以便誘導使用者單擊該連結。
從表面上看,這些URL的域名確實屬於使用者將要(或正在)訪問的合法站點。例如: https://example.com/login?url=http://examp1e.com/bad/things ,確實是屬於example.com 域名。
然而,如果伺服器端的重定向邏輯沒有對URL引數中的資料進行驗證,那麼使用者最終訪問到的網站可能是偽造的釣魚站點(例如 examp1e.com )。
這只是攻擊者如何利用不安全的重定向邏輯的一個簡單示例。
不安全重定向的示例
在下面的程式碼中,我們看到/login接收來自URL引數的未經驗證的資料,並且直接就將其傳遞給Express的res.redirect()方法。因此,只要使用者通過身份驗證,Express就會將使用者重定向到輸入或提供的任何URL。
var express = require('express'); var port = process.env.PORT || 3000; var app = express(); app.get('/login', function (req, res, next) { if(req.session.isAuthenticated()) { res.redirect(req.query.url); } }); app.get('/account', function (req, res, next) { res.send('Account page'); }); app.get('/profile', function (req, res, next) { res.send('Profile page'); }); app.listen(port, function() { console.log('Server listening on port ' + port); });
防範方案1:輸入驗證
通常情況下,應該儘可能避免在程式碼中用到重定向和轉發。
如果必須要在程式碼中使用重定向,那麼首選的方案是使用事先定義的對映到特定目標的鍵。這種方案被稱為白名單方法。具體實現方式如下:
1、baseHostname確保任何重定向都將不會離開我們的域名。
2、redirectMapping是一個物件,它將事先定義的鍵(例如傳遞給URL引數的內容)對映到伺服器上的指定路徑。
3、validateRedirect()方法判斷事先定義的鍵是否存在。如果存在,就會返回重定向到相應路徑。
4、修改/login邏輯,將baseHostname和redirectPath變數連線在一起,避免使使用者提供的輸入內容直接進入到Express的res.redirect()方法。
5、最後,使用encodeURI()方法作為額外的保證,確保串聯字串的URI部分被正確編碼,只允許合法的重定向。
參考程式碼如下:
//Configure your whitelist var baseHostname = "https://example.com"; var redirectMapping = { 'account': '/account', 'profile': '/profile' } //Create a function to validate whitelist function validateRedirect(key) { if(key in redirectMapping) { return redirectMapping[key]; }else{ return false; } } app.get('/login', function (req, res, next) { if(req.session.isAuthenticated()) { redirectPath = validateRedirect(req.query.url); if(redirectPath) { res.redirect(encodeURI(baseHostname + redirectPath)); }else{ res.send('Not a valid redirect!'); } } });
防範方案2
在某些情況下,將每種合法的重定向列成白名單是不切實際的,但開發者仍然需要進行重定向,並且希望將重定向限制在域的範圍內。這時,如果外部提供的值能夠遵循特定模式(例如:都是16個字元的字串,由字母和數字組成),可以採用這種方案。僅包含字母和數字的字串是最為理想的,因為它們不包含任何可能有助於目錄遍歷、路徑遍歷等攻擊的特殊字元(例如省略號、斜槓等)。
舉例來說,開發人員可能希望使用者在登陸後,能重定向回到電子商務網站上的特定專案。由於電子商務網站針對每種產品都有一個唯一的值,由字母和數字組成,因此開發者可以根據RegEx白名單驗證外部輸入,從而實現安全的重定向。在這種情況下,使用的是productId變數,如下面程式碼所示:
//配置白名單 var baseHostname = "https://example.com"; app.get('/login', function (req, res, next) { productId = (req.query.productId || ''); whitelistRegEx = /^[a-zA-Z0-9]{16}$/; if(productId) { //Validate the productId is alphanumeric and exactly 16 characters if(whitelistRegEx.test(productId)) { res.redirect(encodeURI(baseHostname + '/item/' + productId)); }else{ //The productId did not meet the RegEx whitelist, so return an error res.send('Invalid product ID'); } }else{ //No productId was provided, so redirect to home page res.redirect('/'); } });
最後,警告使用者他們正在被自動重定向是非常重要的。如果實際上需要將使用者重定向到域名之外,那麼可能需要在流程中建立一箇中間頁面,如下圖所示,該頁面能夠提醒使用者,並明確告知將要重定向到的URL。