概念
對于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。