:XSS、CSRF與SQL注入防御)
Go安全防護(hù):XSS、CSRF與SQL注入防御摘要: 本篇講解Go語言Web安全防護(hù)實戰(zhàn)使用html/template自動轉(zhuǎn)義防XSS實現(xiàn)CSRF token中間件用參數(shù)化查詢防SQL注入設(shè)置安全HTTP響應(yīng)頭分享模板自動轉(zhuǎn)義導(dǎo)致JSON輸出損壞的踩坑經(jīng)驗對比模板轉(zhuǎn)義、手動轉(zhuǎn)義、CSP三種XSS防御方案。開篇故事去年做一個內(nèi)容管理系統(tǒng)用戶可以在后臺編輯HTML格式的文章。第一版直接用text/template渲染用戶內(nèi)容上線當(dāng)天就被人塞了段scriptalert(xss)/script進(jìn)去。所有訪問該文章的用戶瀏覽器彈了個框客服接到十幾個投訴。當(dāng)時我以為換個html/template包就完事了html/template會自動轉(zhuǎn)義。換完確實彈框沒了但新的坑又來了后臺有個接口需要返回JSON格式的富文本數(shù)據(jù)給前端html/template的自動轉(zhuǎn)義把JSON里的引號也轉(zhuǎn)義了前端解析直接報錯。花了大半天才搞明白html/template的上下文轉(zhuǎn)義機(jī)制。這篇把XSS、CSRF、SQL注入三大Web安全漏洞在Go里的防御方法寫清楚。一、XSS防御:html/template上下文轉(zhuǎn)義Go標(biāo)準(zhǔn)庫的html/template包是防XSS的核心工具。它根據(jù)內(nèi)容出現(xiàn)的上下文做不同的轉(zhuǎn)義在HTML標(biāo)簽里轉(zhuǎn)義方式跟在JavaScript里不同在URL屬性里又不同。packagemainimport(html/templatenet/httpnet/urlgithub.com/microcosm-cc/bluemonday)// PageData 模板數(shù)據(jù)結(jié)構(gòu)typePageDatastruct{TitlestringContentstring// 用戶輸入的HTML內(nèi)容Authorstring}// SafeTemplate 安全的HTML模板渲染// html/template根據(jù)上下文自動轉(zhuǎn)義consttemplateHTML !DOCTYPE html html head title{{.Title}}/title script // 在JavaScript上下文中模板自動轉(zhuǎn)義引號和特殊字符 // 防止通過JS注入惡意代碼 var author {{.Author}}; var content {{.Content | jsEscaper}}; /script /head body h1{{.Title}}/h1 !-- 在HTML上下文中自動轉(zhuǎn)義 -- div classcontent !-- 這里Content是用戶輸入需要轉(zhuǎn)義 -- {{.Content | htmlEscape}} /div a href/search?q{{.Author | urlQueryEscape}}搜索作者/a /body /html // 全局模板實例預(yù)編譯提高性能vartmpltemplate.Must(template.New(page).Funcs(template.FuncMap{// 自定義HTML轉(zhuǎn)義函數(shù)// 對于用戶提供的富文本HTML需要先做白名單過濾再轉(zhuǎn)義htmlEscape:func(sstring)template.HTML{// 白名單過濾允許的HTML標(biāo)簽// 只保留p br strong em a img等安全標(biāo)簽// 過濾掉script iframe object等危險標(biāo)簽p:bluemonday.UGCPolicy()returntemplate.HTML(p.Sanitize(s))},// JS轉(zhuǎn)義防止XSS通過JS注入jsEscaper:func(sstring)template.JS{returntemplate.JS(s)},// URL查詢轉(zhuǎn)義urlQueryEscape:func(sstring)string{// 用net/url的QueryEscape做URL編碼returnurl.QueryEscape(s)},},).Parse(templateHTML))// RenderPage 渲染頁面funcRenderPage(w http.ResponseWriter,data PageData){w.Header().Set(Content-Type,text/html; charsetutf-8)// Execute內(nèi)部做了上下文轉(zhuǎn)義iferr:tmpl.Execute(w,data);err!nil{http.Error(w,渲染失敗,http.StatusInternalServerError)}}funcmain(){mux:http.NewServeMux()mux.HandleFunc(/,func(w http.ResponseWriter,r*http.Request){// 模擬用戶輸入包含XSS嘗試data:PageData{Title:文章標(biāo)題,Content:scriptalert(xss)/scriptp正文內(nèi)容/p,Author:;alert(1);//,}RenderPage(w,data)})http.ListenAndServe(:8080,mux)}html/template的轉(zhuǎn)義機(jī)制依賴模板中變量出現(xiàn)的位置。變量在{{.Content}}的位置決定轉(zhuǎn)義策略在HTML標(biāo)簽內(nèi)做HTML轉(zhuǎn)義在script標(biāo)簽內(nèi)做JS轉(zhuǎn)義在URL屬性內(nèi)做URL轉(zhuǎn)義。用template.HTML類型包裝內(nèi)容會跳過轉(zhuǎn)義所以富文本過濾必須嚴(yán)格。二、CSRF中間件與SQL注入防御CSRF(跨站請求偽造)攻擊利用用戶的登錄態(tài)誘導(dǎo)用戶在不知情的情況下發(fā)送請求。防御方法是給表單加一個隨機(jī)token服務(wù)端驗證token是否匹配。packagemiddlewareimport(crypto/randdatabase/sqlencoding/hexnet/httpstringsgithub.com/gorilla/csrf)// CSRFMiddleware CSRF防護(hù)中間件// 使用gorilla/csrf實現(xiàn)雙提交Cookie模式// 前端通過meta標(biāo)簽獲取tokenAjax請求帶在header里funcCSRFMiddleware(next http.Handler)http.Handler{// 32字節(jié)密鑰用于簽名token// 生產(chǎn)環(huán)境從配置中心讀取不要硬編碼key:make([]byte,32)rand.Read(key)// Secure為true時只在HTTPS下發(fā)送cookie// HttpOnly防止JS讀取cookiereturncsrf.Protect(key,csrf.Secure(false),// 開發(fā)環(huán)境關(guān)閉生產(chǎn)環(huán)境改truecsrf.HttpOnly(true),// JS無法讀取CSRF cookiecsrf.SameSite(csrf.SameSiteStrictMode),// 嚴(yán)格同源策略csrf.RequestHeader(X-CSRF-Token),// 前端從header傳tokencsrf.FieldName(csrf_token),// 表單字段名)(next)}// SafeQuery 安全的參數(shù)化查詢// 使用占位符?數(shù)據(jù)庫驅(qū)動自動做轉(zhuǎn)義// 這是防SQL注入的核心手段funcSafeQuery(db*sql.DB,userIDstring)(*User,error){// 使用參數(shù)化查詢userID作為參數(shù)傳入// 驅(qū)動會自動處理特殊字符不會拼接到SQL語句中query:SELECT id, name, email FROM users WHERE id ?row:db.QueryRow(query,userID)varu User err:row.Scan(u.ID,u.Name,u.Email)iferr!nil{returnnil,err}returnu,nil}// SafeSearch 安全的模糊搜索// 防止通過搜索框注入SQLfuncSafeSearch(db*sql.DB,keywordstring)([]User,error){// LIKE查詢也要用參數(shù)化// %和_是LIKE的特殊字符需要轉(zhuǎn)義keywordstrings.ReplaceAll(keyword,%,\\%)keywordstrings.ReplaceAll(keyword,_,\\_)// 拼接LIKE的通配符pattern:%keyword%query:SELECT id, name FROM users WHERE name LIKE ? ESCAPE \\rows,err:db.Query(query,pattern)iferr!nil{returnnil,err}deferrows.Close()varusers[]Userforrows.Next(){varu Useriferr:rows.Scan(u.ID,u.Name);err!nil{returnnil,err}usersappend(users,u)}returnusers,nil}// SafeBatchInsert 安全的批量插入// 防止批量操作時SQL注入funcSafeBatchInsert(db*sql.DB,users[]User)error{// 開啟事務(wù)保證原子性tx,err:db.Begin()iferr!nil{returnerr}defertx.Rollback()// 預(yù)編譯SQL提高批量操作性能stmt,err:tx.Prepare(INSERT INTO users (id, name) VALUES (?, ?))iferr!nil{returnerr}deferstmt.Close()for_,u:rangeusers{// 每個參數(shù)都通過占位符傳入if_,err:stmt.Exec(u.ID,u.Name);err!nil{returnerr}}returntx.Commit()}// User 用戶結(jié)構(gòu)體typeUserstruct{IDstringNamestringEmailstring}// GenerateCSRFToken 生成隨機(jī)CSRF token// 用于手動實現(xiàn)CSRF防護(hù)的場景funcGenerateCSRFToken()(string,error){b:make([]byte,32)if_,err:rand.Read(b);err!nil{return,err}// 轉(zhuǎn)成十六進(jìn)制字符串returnhex.EncodeToString(b),nil}參數(shù)化查詢是防SQL注入的根本手段。原理是SQL引擎先編譯SQL模板再把參數(shù)填充進(jìn)去參數(shù)不會作為SQL語法的一部分被解析。即使用戶輸入 OR 11也只會被當(dāng)成一個字符串值處理不會改變SQL語義。三、獨家踩坑:模板自動轉(zhuǎn)義損壞JSON輸出這個坑發(fā)生在前后端分離的項目里。后端用html/template渲染一個返回JSON的接口前端拿到的JSON解析報錯。排查發(fā)現(xiàn)JSON字符串里的雙引號被轉(zhuǎn)義成了#34;。問題出在html/template的上下文感知機(jī)制。當(dāng)模板檢測到輸出在script標(biāo)簽內(nèi)時它會把內(nèi)容當(dāng)作JavaScript處理對字符串中的雙引號、尖號做HTML實體轉(zhuǎn)義。這導(dǎo)致JSON結(jié)構(gòu)被破壞。packagemainimport(encoding/jsonnet/httpstrings)// JSONAPIData API響應(yīng)數(shù)據(jù)typeJSONAPIDatastruct{Articles[]Articlejson:articles}// Article 文章結(jié)構(gòu)typeArticlestruct{IDstringjson:idTitlestringjson:title}// RenderJSONAPI 正確的JSON API響應(yīng)// 用encoding/json而不是html/template渲染JSONfuncRenderJSONAPI(w http.ResponseWriter,datainterface{}){w.Header().Set(Content-Type,application/json; charsetutf-8)// 直接用encoding/json編碼// 不經(jīng)過html/template避免自動轉(zhuǎn)義破壞JSONencoder:json.NewEncoder(w)// 防止XSS: 設(shè)置EscapeHTML轉(zhuǎn)義HTML特殊字符// 這會把 轉(zhuǎn)成\u003c \u003e \u0026// 安全且不破壞JSON結(jié)構(gòu)encoder.SetEscapeHTML(true)encoder.SetIndent(, )iferr:encoder.Encode(data);err!nil{http.Error(w,JSON編碼失敗,http.StatusInternalServerError)}}// SafeRenderInScript 在script標(biāo)簽內(nèi)安全渲染JSON// 當(dāng)必須在HTML模板的script標(biāo)簽內(nèi)輸出JSON時使用funcSafeRenderInScript(w http.ResponseWriter,datainterface{}){w.Header().Set(Content-Type,text/html; charsetutf-8)// 先用encoding/json編碼JSON結(jié)構(gòu)不會被破壞jsonBytes,err:json.Marshal(data)iferr!nil{http.Error(w,編碼失敗,http.StatusInternalServerError)return}// 在script標(biāo)簽內(nèi)輸出// json.Marshal默認(rèn)不會轉(zhuǎn)義HTML特殊字符(除非SetEscapeHTML(true))// 但在script標(biāo)簽內(nèi)需要防止/script注入// 替換掉/script是關(guān)鍵safeJSON:string(jsonBytes)safeJSONreplaceScriptClose(safeJSON)w.Write([]byte(scriptwindow.__DATA__safeJSON;/script))}// replaceScriptClose 替換關(guān)閉script標(biāo)簽的字符串// 防止用戶內(nèi)容提前關(guān)閉script標(biāo)簽funcreplaceScriptClose(sstring)string{// 把/script替換成\/script// JS中\(zhòng)/script不會被解析為標(biāo)簽關(guān)閉sstrings.ReplaceAll(s,/script,\\/script)sstrings.ReplaceAll(s,!--,\\!--)sstrings.ReplaceAll(s,--,--\\)returns}前后端分離架構(gòu)里JSON API不要用html/template渲染。用encoding/json編碼同時開SetEscapeHTML(true)把、、轉(zhuǎn)義成Unicode轉(zhuǎn)義序列既防XSS又不破壞JSON結(jié)構(gòu)。如果非要在script標(biāo)簽內(nèi)輸出JSON必須替換掉/script字符串防止用戶內(nèi)容提前關(guān)閉標(biāo)簽。四、安全響應(yīng)頭設(shè)置安全響應(yīng)頭是Web安全的補(bǔ)充防線。即使應(yīng)用層有漏洞響應(yīng)頭也能增加攻擊難度。packagemiddlewareimport(net/http)// SecurityHeaders 安全響應(yīng)頭中間件// 補(bǔ)充TLS之外的安全防護(hù)funcSecurityHeaders(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){h:w.Header()// CSP: 內(nèi)容安全策略限制資源加載來源// 最有效的XSS防護(hù)手段之一h.Set(Content-Security-Policy,default-src self; script-src self unsafe-inline; style-src self unsafe-inline; img-src self data: https:; connect-src self; frame-ancestors none)// HSTS: 強(qiáng)制HTTPSh.Set(Strict-Transport-Security,max-age31536000; includeSubDomains)// 防MIME嗅探h.Set(X-Content-Type-Options,nosniff)// 防點擊劫持h.Set(X-Frame-Options,DENY)// Referer控制h.Set(Referrer-Policy,strict-origin-when-cross-origin)// 權(quán)限策略禁用不必要的瀏覽器功能h.Set(Permissions-Policy,geolocation(), microphone(), camera())next.ServeHTTP(w,r)})}CSP是響應(yīng)頭里最強(qiáng)大的防護(hù)。設(shè)置script-src self后只允許加載同源腳本內(nèi)聯(lián)腳本和外部CDN腳本都被拒絕。配合nonce或hash可以更精確地控制允許執(zhí)行的腳本。五、對比分析XSS防御方案防護(hù)強(qiáng)度開發(fā)成本性能開銷適用場景html/template轉(zhuǎn)義高低(自動)極低服務(wù)端渲染手動轉(zhuǎn)義中高(易遺漏)極低特殊場景CSP響應(yīng)頭高中(需調(diào)試)無全場景補(bǔ)充富文本白名單過濾高高(需維護(hù)規(guī)則)中用戶富文本html/template自動轉(zhuǎn)義是服務(wù)端渲染的首選開箱即用且不易遺漏。手動轉(zhuǎn)義容易遺漏某些字段維護(hù)成本高。CSP是客戶端層面的補(bǔ)充防線限制腳本執(zhí)行來源。用戶提供的富文本HTML需要先白名單過濾再轉(zhuǎn)義bluemonday庫是Go里常用的HTML過濾器。總結(jié)XSS防御靠html/template的上下文轉(zhuǎn)義前后端分離的JSON API用encoding/json加SetEscapeHTML(true)。CSRF防御用gorilla/csrf中間件配合同源cookie策略。SQL注入防御靠參數(shù)化查詢所有用戶輸入都通過占位符傳入。安全響應(yīng)頭特別是CSP是最后的補(bǔ)充防線。上一篇講了TLS傳輸層加密這篇把應(yīng)用層安全防護(hù)講完下一篇進(jìn)入日志系統(tǒng)講zap結(jié)構(gòu)化日志的實戰(zhàn)。