摘要: 本文討論web前端安全問題以及應對措施,瀏覽器同源策略以及對資源跨域訪問的幾種解決方案
本文分享自華為雲社群《Web安全和瀏覽器跨域訪問》,原文作者:kg-follower 。
今天說一說和前端相關的Web安全問題和開發過程中經常遇到的跨域問題。
1.Web安全
1.1 XSS
基本原理
XSS (Cross-Site Scripting),跨站指令碼攻擊通過在使用者的瀏覽器內執行非法的HTML標籤或JavaScript進行的一種攻擊。
攻擊手段
攻擊者往 Web 頁面裡插入惡意網頁尾本程式碼,當使用者瀏覽該頁面時,嵌入 Web 頁面裡面的指令碼程式碼會被執行,從而達到攻擊者盜取使用者資訊或其他侵犯使用者安全隱私的目的。
XSS攻擊分類
反射型xss攻擊。通過給被攻擊者傳送帶有惡意指令碼的URL或將不可信內容插入頁面,當URL地址被開啟或頁面被執行時,瀏覽器解析、執行惡意指令碼。
反射型xss的攻擊步驟:1. 攻擊者構造出特殊的 URL或特殊資料;2. 使用者開啟帶有惡意程式碼的 URL 時,Web伺服器將惡意程式碼從 URL 中取出,拼接在 HTML 中返回給瀏覽器;3. 使用者瀏覽器接收到響應後解析執行,混在其中的惡意程式碼也被執行;4. 惡意程式碼竊取使用者資料併傳送到攻擊者的網站,或者冒充使用者的行為,呼叫目標網站介面執行攻擊者指定的操作。
防禦:1.Web頁面渲染的所有內容或資料都必須來自服務端;2. 客戶端對使用者輸入的內容進行安全符轉義,服務端對上交內容進行安全轉義;3.避免拼接html。
儲存型xss。惡意指令碼被儲存在目標伺服器上。當瀏覽器請求資料時,指令碼從伺服器傳回瀏覽器去執行。
儲存型xss的攻擊步驟:1. 攻擊者將惡意程式碼提交到目標網站的資料庫中;2.使用者瀏覽到目標網站時,前端頁面獲得資料庫中讀出的惡意指令碼時將其渲染執行。
防禦:防範儲存型XSS攻擊,需要我們增加字串的過濾:前端輸入時過濾;服務端增加過濾;前端輸出時過濾。
通常有三種方式防禦XSS攻擊:1. Content Security Policy(CSP)。CSP 本質上就是建立白名單,開發者明確告訴瀏覽器哪些外部資源可以載入和執行。我們只需要配置規則,如何攔截是由瀏覽器自己實現的。我們可以通過這種方式來儘量減少 XSS 攻擊。通常可以通過兩種方式開啟,例如只允許載入相同域下的資源:
設定 HTTP Header 中的 CSP(Content-Security-Policy: default-src 'self')
設定meta 標籤的方式(<meta http-equiv="Content-Security-Policy" content="form-action 'self';">)
2. 轉義字元。使用者的輸入永遠不可信任的,最普遍的做法就是轉義輸入輸出的內容,對於引號、尖括號、斜槓進行轉義:
function escape(str) { str = str.replace(/&/g, '&') str = str.replace(/</g, '<') str = str.replace(/>/g, '>') str = str.replace(/"/g, '&quto;') str = str.replace(/'/g, ''') str = str.replace(/`/g, '`') str = str.replace(/\//g, '/') return str }
但是對於顯示富文字來說,顯然不能通過上面的辦法來轉義所有字元,因為這樣會把需要的格式也過濾掉。對於這種情況,通常採用白名單過濾的辦法:
const xss = require('xss') let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>') console.log(html) <h1>XSS Demo</h1><script>alert("xss");</script>
經過白名單過濾,dom中包含的<script>標籤將不會被執行。
HTTP-only Cookie: 禁止 JavaScript 讀取某些敏感 cookie,使得 cookie只有http能夠訪問。
1.2 CSRF
基本概念
CSRF(Cross-site request forgery跨站請求偽造:攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站傳送跨站請求。利用受害者在被攻擊網站已經獲取的註冊憑證,繞過後臺的使用者驗證,達到冒充使用者對被攻擊的網站執行某項操作的目的。
CSRF攻擊型別
主動型攻擊。使用者訪問網站A並在瀏覽器儲存A的登入狀態(cookie等資訊),攻擊者誘導受害者訪問網站B,網站B含有訪問A介面的惡意程式碼,受害者訪問B時帶著A的登入狀態,攻擊者便可以冒充使用者執行對A的惡意操作。
被動型攻擊。攻擊者在網站A釋出帶有惡意連結的評論或內容(提交對A帶有增刪改的誘導型標籤),當其他擁有登入狀態的受害者點選評論的惡意連結時,就會冒用受害者登入憑證發起攻擊。
CSRF攻擊防範
驗證HTTP Referer欄位。在HTTP頭中有Referer欄位,他記錄該HTTP請求的來源地址,如果跳轉的網站與來源地址相符,那就是合法的,如果不符則可能是csrf攻擊,拒絕該請求。
SameSite。可以對 Cookie 設定 SameSite 屬性。該屬性表示 Cookie 不隨著跨域請求傳送,可以很大程度減少 CSRF 的攻擊。
請求中加入token。服務端給使用者生成一個token,加密後傳遞給使用者,使用者在提交請求時,需要攜帶這個token,服務端發現token不存在或者token校驗不成功,那麼就拒絕該請求。
1.3 流量劫持
DNS劫持
DNS劫持就是通過劫持了DNS伺服器,通過某些手段來取得某個域名的解析控制權,進而修改此域名的解析結果,導致對該域名的訪問由原IP地址轉入到修改後的IP,其結果就是對特定的網站不能訪問或訪問的是假網址。
防禦:使用https校驗通訊雙方身份和資料完整性。
點選劫持
攻擊者構建了一個非常有吸引力的網頁,將被攻擊的頁面放置在當前頁面的 iframe 中,使用樣式將 iframe 疊加到非常有吸引力內容的上方,將iframe設定為100%透明,其實就是通過覆蓋不可見的頁面,誘導使用者點選而造成的攻擊行為。
防禦措施。1. X-FRAME-OPTIONS設定允許iframe載入的域 2. 限制iframe頁面中的JavaScript指令碼執行。
無論是xss、csrf還是點選劫持,上面討論的這幾種攻擊屬於前端攻擊,原因大多是開發者的指令碼或模板程式碼存在不安全的隱患或是沒有考慮網路傳輸安全問題。下面簡單說一說惡意攻擊利用網站後臺漏洞發起的攻擊。
1.4 SQL隱碼攻擊
SQL 注入漏洞存在的原因,就是拼接 SQL 引數。也就是將用於輸入的查詢引數,直接拼接在 SQL 語句中,惡意攻擊者可以構造特殊的sql語句繞過安全驗證。
SQL隱碼攻擊條件:1.攻擊者可以控制輸入的資料;2.伺服器要執行的程式碼拼接了被控制的資料。
SQL隱碼攻擊防禦。1. 嚴格限制Web應用的資料庫的操作許可權;2. 對進入資料庫的特殊字元(’,”,,<,>,&,*,; 等)進行轉義處理,或編碼轉換,類似防禦xss攻擊時對輸入轉義;3. 所有的查詢語句建議使用資料庫提供的引數化查詢介面,如使用佔位引數或物件關係對映ORM。
1.5 DDOS攻擊
DOS攻擊通過在網站的各個環節進行攻擊,使得整個流程跑不起來,以達到癱瘓服務為目的。最常見的就是傳送大量請求導致伺服器過載當機。DDOS攻擊的原理就是利用分散式的客戶端,向目標發起大量看上去合法的請求,消耗/佔用大量資源,從而達到拒絕服務的目的。
攻擊方式:1.埠掃描;2.ping洪水;3.SYN洪水;4.FTP跳轉攻擊;
DDOS防範。1.在伺服器上刪除未使用的服務,關閉未使用的埠。2. 進行實時監控,封禁某些惡意密集型請求IP段;3. 進行靜態資源快取,隔離原始檔的訪問,比如CDN加速;4. 隱藏伺服器的真實IP地址
3 跨域和同源策略
同源策略是一個重要的安全策略,它用於限制一個源的文件或者它載入的指令碼如何能與另一個源的資源進行互動。它能幫助阻隔惡意文件,減少可能被攻擊的媒介。所謂同源是指“協議+域名+埠”三者均相同。
同源策略限制了客戶端js程式碼的以下行為:
1.Cookie、LocalStorage 和 IndexDB 無法讀取;
2.DOM節點。來自一個源的js只能讀寫自己源的DOM樹不能讀取其他源的DOM樹。如果兩個網頁不同源,就無法拿到對方的DOM。典型的例子是iframe視窗和window.open方法開啟的視窗,它們與父視窗無法通訊。
網站不開啟同源策略,釣魚網站便可以使用iframe標籤載入中國銀行登入介面,執行指令碼進而拿到使用者名稱密碼。
當設定了同源策略,父子視窗執行獲取對方DOM時會報錯。
3.AJAX請求限制
跨域並不是請求發不出去,請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了。
除了架設伺服器代理,還有以下幾種方法規避同源限制:JSONP,WebSocket,CORS,本文詳細討論下後兩種方法的實現。
WebSocket。WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)作為協議字首。該協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。WebSocket 是一種雙向通訊協議,在建立連線之後,WebSocket 的 server 與 client 都能主動向對方傳送或接收資料。Websocket請求頭資訊包含一個origin欄位,伺服器根據這個欄位判斷是否允許本次通訊。
CORS。CORS跨域資源共享是W3C標準,是解決跨域Ajax請求的最常見解決方法。整個CORS通訊過程,都是瀏覽器自動完成,不需要使用者參與。對於開發者來說,CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動新增一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。只要同時滿足以下兩大條件,就屬於簡單請求:
(1) 請求方法是以下三種方法之一:HEAD、GET、POST
(2)HTTP的頭資訊不超出以下幾種欄位:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
對於簡單請求,瀏覽器直接發出CORS請求。具體來說,就是在頭資訊之中,增加一個Origin欄位,該欄位用來說明,本次請求來自哪個源。伺服器根據這個值,決定是否同意這次請求。如果Origin指定的源,不在許可範圍內,伺服器會返回一個正常的HTTP回應。若該響應的頭資訊沒有包含Access-Control-Allow-Origin欄位,就丟擲一個錯誤,被XMLHttpRequest的onerror回撥函式捕獲。若Origin指定的域名在許可範圍內,伺服器返回的響應,會多出幾個頭資訊欄位。其中Access-Control-Allow-Origin欄位是必須的。它的值要麼是請求時Origin欄位的值,要麼是一個*,表示接受任意域名的請求。
對於非簡單請求,在正式通訊之前,會增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。瀏覽器先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些HTTP方法和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。
"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭資訊裡面,關鍵欄位是Origin,表示請求來自哪個源。
除了Origin欄位,"預檢"請求的頭資訊包括兩個特殊欄位。
(1)Access-Control-Request-Method。該欄位是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法
(2)Access-Control-Request-Headers。該欄位是一個逗號分隔的字串,指定瀏覽器CORS請求會額外傳送的頭資訊欄位。
預檢請求的回應。
伺服器收到"預檢"請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers欄位以後,確認允許跨源請求,就可以做出回應。回應最關鍵的是Access-Control-Allow-Origin欄位,表示允許該源的請求,若沒有任何CORS相關頭資訊欄位則說明伺服器否認該請求。若伺服器允許,則Access-Control-Allow-Methods欄位是必須的,它的值是一個逗號分隔的字串,表明伺服器支援的方法。如果預檢請求包含Access-Control-Request-Headers欄位,則返回體中該欄位也是必須的,它也是一個逗號分隔的字串,表明伺服器支援的所有頭資訊欄位,不限於瀏覽器在"預檢"中請求的欄位。預檢請求得到允許回應後,瀏覽器便傳送正常CORS請求。
最近在開發一個前端poc專案時遇到了跨域資源訪問被限制的問題,在本地啟動angular專案,其他人可以通過ip訪問到靜態資源,傳送ajax請求時被限制。於是想通過配置代理的方式解決這個跨域問題:在和package.json同級的目錄中新建proxy.conf.json檔案,target欄位是後端服務真實的ip,changeOrigin欄位設定為true,關閉secure欄位。
{ "/": { "target": "http://10.173.99.224:8081/", "changeOrigin": true, "secure": false, "loglevel": "debug" } }
在package.json的啟動命令中新增
"scripts": { "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json --host 0.0.0.0", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" },
--host 0.0.0.0 表示監聽所有來源的主機。解決