Web 安全概念
Web 應(yīng)用中存在很多安全風(fēng)險,這些風(fēng)險會被黑客利用,輕則篡改網(wǎng)頁內(nèi)容,重則竊取網(wǎng)站內(nèi)部數(shù)據(jù),更為嚴重的則是在網(wǎng)頁中植入惡意代碼,使得用戶受到侵害。常見的安全漏洞如下:
XSS 攻擊:對 Web 頁面注入腳本,使用 java script 竊取用戶信息,誘導(dǎo)用戶操作。
CSRF 攻擊:偽造用戶請求向網(wǎng)站發(fā)起惡意請求。
釣魚攻擊:利用網(wǎng)站的跳轉(zhuǎn)鏈接或者圖片制造釣魚陷阱。
HTTP參數(shù)污染:利用對參數(shù)格式驗證的不完善,對服務(wù)器進行參數(shù)注入攻擊。
遠程代碼執(zhí)行:用戶通過瀏覽器提交執(zhí)行命令,由于服務(wù)器端沒有針對執(zhí)行函數(shù)做過濾,導(dǎo)致在沒有指定絕對路徑的情況下就執(zhí)行命令。
而框架本身針對 Web 端常見的安全風(fēng)險內(nèi)置了豐富的解決方案:
利用 extend 機制擴展了 Helper API, 提供了各種模板過濾函數(shù),防止釣魚或 XSS 攻擊。
常見 Web 安全頭的支持。
CSRF 的防御方案。
靈活的安全配置,可以匹配不同的請求 url 。
可定制的白名單,用于安全跳轉(zhuǎn)和 url 過濾。
各種模板相關(guān)的工具函數(shù)做預(yù)處理。
在框架中內(nèi)置了安全插件 egg-security, 提供了默認的安全實踐。
開啟與關(guān)閉配置
注意:除非清楚的確認后果,否則不建議擅自關(guān)閉安全插件提供的功能。
框架的安全插件是默認開啟的,如果我們想關(guān)閉其中一些安全防范,直接設(shè)置該項的 enable 屬性為 false 即可。例如關(guān)閉 xframe 防范:
exports.security = {
xframe: {
enable: false,
},
};
match 和 ignore
match 和 ignore 使用方法和格式與中間件通用配置一致。
如果只想開啟針對某一路徑,則配置 match 選項,例如只針對 /example 開啟 CSP:
exports.security = {
csp: {
match: '/example',
policy: {
//...
},
},
};
如果需要針對某一路徑忽略某安全選項,則配置 ignore 選項,例如針對 /example 關(guān)閉 xframe,以便合作商戶能夠嵌入我們的頁面:
exports.security = {
csp: {
ignore: '/example',
xframe: {
//...
},
},
};
如果要針對內(nèi)部 ip 關(guān)閉部分安全防范:
exports.security = {
csrf: {
// 判斷是否需要 ignore 的方法,請求上下文 context 作為第一個參數(shù)
ignore: ctx => isInnerIp(ctx.ip),
},
}
下面我們會針對具體的場景,來講解如何使用框架提供的安全方案進行 Web 安全防范。
安全威脅XSS的防范
XSS(cross-site scripting跨域腳本攻擊)攻擊是最常見的 Web 攻擊,其重點是『跨域』和『客戶端執(zhí)行』。
XSS 攻擊一般分為兩類:
Reflected XSS(反射型的 XSS 攻擊)
Stored XSS(存儲型的 XSS 攻擊)
Reflected XSS
反射型的 XSS 攻擊,主要是由于服務(wù)端接收到客戶端的不安全輸入,在客戶端觸發(fā)執(zhí)行從而發(fā)起 Web 攻擊。比如:
在某購物網(wǎng)站搜索物品,搜索結(jié)果會顯示搜索的關(guān)鍵詞。搜索關(guān)鍵詞填入, 點擊搜索。頁面沒有對關(guān)鍵詞進行過濾,這段代碼就會直接在頁面上執(zhí)行,彈出 alert。
防范方式
框架提供了 helper.escape() 方法對字符串進行 XSS 過濾。
const str = '><script>alert("abc") </script><';
console.log(ctx.helper.escape(str));
// => ><script>alert("abc") </script><
當(dāng)網(wǎng)站需要直接輸出用戶輸入的結(jié)果時,請務(wù)必使用 helper.escape() 包裹起來,如在 egg-view-nunjucks 里面就覆蓋掉了內(nèi)置的 escape。
另外一種情況,網(wǎng)站輸出的內(nèi)容會提供給 java script 來使用。這個時候需要使用 helper.sjs() 來進行過濾。
helper.sjs() 用于在 java script(包括 onload 等 event)中輸出變量,會對變量中字符進行 java script ENCODE, 將所有非白名單字符轉(zhuǎn)義為 \x 形式,防止 XSS 攻擊,也確保在 js 中輸出的正確性。使用實例:
const foo = '"hello"';
// 未使用 sjs
console.log(`var foo = "${foo}";`);
// => var foo = ""hello"";
// 使用 sjs
console.log(`var foo = "${this.helper.sjs(foo)}";`);
// => var foo = "\\x22hello\\x22";
還有一種情況,有時候我們需要在 java script 中輸出 json ,若未做轉(zhuǎn)義,易被利用為 XSS 漏洞?蚣芴峁┝ helper.sjson() 宏做 json encode,會遍歷 json 中的 key ,將 value 的值中,所有非白名單字符轉(zhuǎn)義為 \x 形式,防止 XSS 攻擊。同時保持 json 結(jié)構(gòu)不變。 若存在模板中輸出一個 JSON 字符串給 java script 使用的場景,請使用 helper.sjson(變量名) 進行轉(zhuǎn)義。
處理過程較復(fù)雜,性能損耗較大,請僅在必要時使用。
實例:
<script>
window.locals = {{ helper.sjson(locals) }};
</script>
Stored XSS
基于存儲的 XSS 攻擊,是通過提交帶有惡意腳本的內(nèi)容存儲在服務(wù)器上,當(dāng)其他人看到這些內(nèi)容時發(fā)起 Web 攻擊。一般提交的內(nèi)容都是通過一些富文本編輯器編輯的,很容易插入危險代碼。
防范方式
框架提供了 helper.shtml() 方法對字符串進行 XSS 過濾。
注意,將富文本(包含 HTML 代碼的文本)當(dāng)成變量直接在模版里面輸出時,需要用到 shtml 來處理。 使用 shtml 可以輸出 HTML 的 tag,同時執(zhí)行 XSS 的過濾動作,過濾掉非法的腳本。
由于是一個非常復(fù)雜的安全處理過程,對服務(wù)器處理性能一定影響,如果不是輸出 HTML,請勿使用。
簡單示例:
// js
const value = `<a href="http://www.domain.com">google</a><script>evilcode…</script>`;
1
// 模板
<html>
<body>
{{ helper.shtml(value) }}
</body>
</html>
// => <a href="http://www.domain.com">google</a><script>evilcode…</script>
1
shtml 在 xss 模塊基礎(chǔ)上增加了針對域名的過濾。
默認規(guī)則
自定義過濾項: http://jsxss.com/zh/options.html
例如只支持 a 標簽,且除了 title 其他屬性都過濾掉: whiteList: {a: [‘title’]}
options:
config.helper.shtml.domainWhiteList: [] 可拓展 href 和 src 中允許的域名白名單。
注意,shtml 使用了嚴格的白名單機制,除了過濾掉 XSS 風(fēng)險的字符串外, 在默認規(guī)則外的 tag 和 attr 都會被過濾掉。
例如 HTML 標簽就不在白名單中,
const html = '<html></html>';
// html
{{ helper.shtml(html) }}
// 輸出空
常見的 data-xx 屬性由于不在白名單中,所以都會被過濾。
所以,一定要注意 shtml 的適用場景,一般是針對來自用戶的富文本輸入,切忌濫用,功能既受到限制,又會影響服務(wù)端性能。 此類場景一般是論壇、評論系統(tǒng)等,即便是論壇等如果不支持 HTML 內(nèi)容輸入,也不要使用此 Helper,直接使用 escape 即可。
JSONP XSS
JSONP 的 callback 參數(shù)非常危險,他有兩種風(fēng)險可能導(dǎo)致 XSS
1、callback 參數(shù)意外截斷js代碼,特殊字符單引號雙引號,換行符均存在風(fēng)險。
2、callback 參數(shù)惡意添加標簽(如
參考 JSONP 安全攻防
框架內(nèi)部使用 jsonp-body 來對 JSONP 請求進行安全防范。
防御內(nèi)容:
callback 函數(shù)名詞最長 50 個字符限制
callback 函數(shù)名只允許 [, ], a-zA-Z0123456789_, $, .,防止一般的 XSS,utf-7 XSS等攻擊。
可定義配置:
callback 默認 _callback,可以重命名。
limit - 函數(shù)名 length 限制,默認 50。
其他 XSS 的防范方式
瀏覽器自身具有一定針對各種攻擊的防范能力,他們一般是通過開啟 Web 安全頭生效的?蚣軆(nèi)置了一些常見的 Web 安全頭的支持。
CSP
W3C 的 Content Security Policy,簡稱 CSP,主要是用來定義頁面可以加載哪些資源,減少 XSS 的發(fā)生。
框架內(nèi)支持 CSP 的配置,不過是默認關(guān)閉的,開啟后可以有效的防止 XSS 攻擊的發(fā)生。要配置 CSP , 需要對 CSP 的 policy 策略有了解,具體細節(jié)可以參考 CSP 是什么。
X-Download-Options:noopen
默認開啟,禁用 IE 下下載框Open按鈕,防止 IE 下下載文件默認被打開 XSS。
X-Content-Type-Options:nosniff
禁用 IE8 自動嗅探 mime 功能例如 text/plain 卻當(dāng)成 text/html 渲染,特別當(dāng)本站點 serve 的內(nèi)容未必可信的時候。
X-XSS-Protection
IE 提供的一些 XSS 檢測與防范,默認開啟
close 默認值false,即設(shè)置為 1; mode=block
安全威脅 CSRF 的防范
CSRF(Cross-site request forgery跨站請求偽造,也被稱為 One Click Attack 或者 Session Riding,通?s寫為 CSRF 或者 XSRF,是一種對網(wǎng)站的惡意利用。 CSRF 攻擊會對網(wǎng)站發(fā)起惡意偽造的請求,嚴重影響網(wǎng)站的安全。因此框架內(nèi)置了 CSRF 防范方案。
防范方式
通常來說,對于 CSRF 攻擊有一些通用的防范方案,簡單的介紹幾種常用的防范方案:
Synchronizer Tokens:通過響應(yīng)頁面時將 token 渲染到頁面上,在 form 表單提交的時候通過隱藏域提交上來。
Double Cookie Defense:將 token 設(shè)置在 Cookie 中,在提交 post 請求的時候提交 Cookie,并通過 header 或者 body 帶上 Cookie 中的 token,服務(wù)端進行對比校驗。
Custom Header:信任帶有特定的 header(例如 X-Requested-With: XMLHttpRequest)的請求。這個方案可以被繞過,所以 rails 和 django 等框架都放棄了該防范方式。
框架結(jié)合了上述幾種防范方式,提供了一個可配置的 CSRF 防范策略。
使用方式
同步表單的 CSRF 校驗
在同步渲染頁面時,在表單請求中增加一個 name 為 _csrf 的 url query,值為 ctx.csrf,這樣用戶在提交這個表單的時候會將 CSRF token 提交上來:
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<button type="submit">upload</button>
</form>
傳遞 CSRF token 的字段可以在配置中改變:
// config/config.default.js
module.exports = {
security: {
csrf: {
queryName: '_csrf', // 通過 query 傳遞 CSRF token 的默認字段為 _csrf
bodyName: '_csrf', // 通過 body 傳遞 CSRF token 的默認字段為 _csrf
},
},
};
為了防范 BREACH 攻擊,通過同步方式渲染到頁面上的 CSRF token 在每次請求時都會變化,egg-view-nunjucks 等 View 插件會自動對 Form 進行注入,對應(yīng)用開發(fā)者無感知。
AJAX 請求
在 CSRF 默認配置下,token 會被設(shè)置在 Cookie 中,在 AJAX 請求的時候,可以從 Cookie 中取到 token,放置到 query、body 或者 header 中發(fā)送給服務(wù)端。
In jQuery:
var csrftoken = Cookies.get('csrfToken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader('x-csrf-token', csrftoken);
}
},
});
通過 header 傳遞 CSRF token 的字段也可以在配置中改變:
// config/config.default.js
module.exports = {
security: {
csrf: {
headerName: 'x-csrf-token', // 通過 header 傳遞 CSRF token 的默認字段為 x-csrf-token
},
},
};
Session vs Cookie 存儲
默認配置下,框架會將 CSRF token 存在 Cookie 中,以方便 AJAX 請求獲取到。但是所有的子域名都可以設(shè)置 Cookie,因此當(dāng)我們的應(yīng)用處于無法保證所有的子域名都受控的情況下,存放在 Cookie 中可能有被 CSRF 攻擊的風(fēng)險?蚣芴峁┝艘粋配置項,可以將 token 存放到 Session 中。
// config/config.default.js
module.exports = {
security: {
csrf: {
useSession: true, // 默認為 false,當(dāng)設(shè)置為 true 時,將會把 csrf token 保存到 Session 中
cookieName: 'csrfToken', // Cookie 中的字段名,默認為 csrfToken
sessionName: 'csrfToken', // Session 中的字段名,默認為 csrfToken
},
},
};
忽略 JSON 請求(已廢棄)
注意:該選項已廢棄,攻擊者可以通過 flash + 307 來攻破,請不要在生產(chǎn)環(huán)境打開改選項!
在 SOP 的安全策略保護下,基本上所有的現(xiàn)代瀏覽器都不允許跨域發(fā)起 content-type 為 JSON 的請求,因此我們可以直接放過類型的 JSON 格式的請求。
// config/config.default.js
module.exports = {
security: {
csrf: {
ignoreJSON: true, // 默認為 false,當(dāng)設(shè)置為 true 時,將會放過所有 content-type 為 `application/json` 的請求
},
},
};
1
2
3
4
5
6
7
8
刷新 CSRF token
當(dāng) CSRF token 存儲在 Cookie 中時,一旦在同一個瀏覽器上發(fā)生用戶切換,新登陸的用戶將會依舊使用舊的 token(之前用戶使用的),這會帶來一定的安全風(fēng)險,因此在每次用戶登陸的時候都必須刷新 CSRF token。
// login controller
exports.login = function* (ctx) {
const { username, password } = ctx.request.body;
const user = yield ctx.service.user.find({ username, password });
if (!user) ctx.throw(403);
ctx.session = { user };
// 調(diào)用 rotateCsrfSecret 刷新用戶的 CSRF token
ctx.rotateCsrfSecret();
ctx.body = { success: true };
}
安全威脅 XST 的防范
XST 的全稱是 Cross-Site Tracing,客戶端發(fā) TRACE 請求至服務(wù)器,如果服務(wù)器按照標準實現(xiàn)了 TRACE 響應(yīng),則在 response body 里會返回此次請求的完整頭信息。通過這種方式,客戶端可以獲取某些敏感的頭字段,例如 httpOnly 的 Cookie。
下面我們基于 Koa 來實現(xiàn)一個簡單的支持 TRACE 方法的服務(wù)器:
var koa = require('koa');
var app = koa();
app.use(function* (next) {
this.cookies.set('a', 1, { httpOnly: true });
if (this.method === 'TRACE') {
var body = '';
for (header in this.headers) {
body += header + ': ' + this.headers[header] + '\r\n';
}
this.body = body;
}
yield* next;
});
app.listen(7001);
啟動服務(wù)后,先發(fā)個 GET 請求 curl -i http://127.0.0.1:7001,得到如下響應(yīng):
HTTP/1.1 200 OK
X-Powered-By: koa
Set-Cookie: a=1; path=/; httponly
Content-Type: text/plain; charset=utf-8
Content-Length: 2
Date: Thu, 06 Nov 2014 05:04:42 GMT
Connection: keep-alive
服務(wù)器設(shè)置了一個 httpOnly 的 Cookie 為 1,在瀏覽器環(huán)境中,是無法通過腳本獲取它的。
接著我們發(fā) TRACE 請求到服務(wù)器curl -X TRACE -b a=1 -i http://127.0.0.1:7001,并帶上 Cookie,得到如下響應(yīng):
HTTP/1.1 200 OK
X-Powered-By: koa
Set-Cookie: a=1; path=/; httponly
Content-Type: text/plain; charset=utf-8
Content-Length: 73
Date: Thu, 06 Nov 2014 05:07:47 GMT
Connection: keep-alive
user-agent: curl/7.37.1
host: 127.0.0.1:7001
accept: */*
cookie: a=1
在響應(yīng)體里可以看到完整的頭信息,這樣我們就繞過了 httpOnly 的限制,拿到了cookie=1,造成了很大的風(fēng)險。