☰
確保輸入過濾:Go Web 應用的資料驗證三步驟與 CleanMap 白名單實踐
2026/10/7 9:32:30 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

過濾使用者資料是 Web 應用安全的基石——多數漏洞(CSRF、XSS、SQL 注入)的根源,都是對外部輸入的「誤信」與「誤用」。本篇文章以《build-web-application-with-golang》9.2 小節為核心,系統講解輸入過濾的「識別資料 → 過濾資料 → 區分已過濾與被汙染資料」三步驟,並結合格語言標準庫的strconv、string、regexp套件以及倉庫內的實際驗證器程式碼,讓你能夠寫出可複製、可執行的白名單過濾邏輯,從源頭堵住惡意資料的注入。

為什麼要把輸入過濾當成安全的第一道防線

過濾使用者資料是 Web 應用安全的基礎。它是驗證資料合法性的過程。透過對所有的輸入資料進行過濾,可以避免惡意資料在程式中被誤信或誤用。大多數 Web 應用的漏洞都是因為沒有對使用者輸入的資料進行恰當過濾所引起的。

這段話點出了 Web 安全最重要的心法:在驗證之前,任何外部資料都必須被視為不安全資料。如果直接把不安全的資料輸出到客戶端,就可能造成跨站指令碼攻擊(XSS,見 9.3 小節);如果把它用於資料庫查詢,就可能造成 SQL 注入。而本章前一節介紹的 CSRF 攻擊 同樣源於對請求資料缺乏嚴格校驗。

所謂「輸入」,並不僅限於鍵盤敲入的內容。凡是源自非程式碼內部提供的資料都屬於需要過濾的範疇:所有來自客戶端的資料、資料庫中的記錄、第三方提供的介面資料等。因此,一個完整的過濾方案應該分為三個步驟:

  1. 識別資料:搞清楚需要過濾的資料來自哪裡;
  2. 過濾資料:弄明白我們需要什麼樣的資料;
  3. 區分已過濾及被汙染資料:如果存在攻擊資料,保證過濾之後可以使用更安全的資料。

下面逐一展開。

識別資料:先搞清楚「資料是什麼、來自哪裡」

「識別資料」之所以是第一步,是因為在不知道「資料是什麼、它來自於哪裡」的前提下,你就不可能正確地過濾它。這裡的資料指所有源自非程式碼內部提供的資料,例如:

  • 所有來自客戶端的資料(最常見的輸入來源);
  • 資料庫中由外部寫入的記錄;
  • 第三方提供的介面資料等。

Go 中最容易識別的輸入:r.Form

由使用者輸入的資料,在 Go 中非常容易識別:透過r.ParseForm()之後,Go 會把使用者 POST 和 GET 的資料全部放在r.Form裡面。之後無論是用r.Form.Get("name")取得單個欄位,還是遍歷r.Form檢查所有欄位,都能拿到原始輸入。

倉庫中的範例 ch.4.4/main.go 展示了標準用法:

func checkProfile(w http.ResponseWriter, r *http.Request) { var errs validator.Errors r.ParseForm() token := r.Form.Get("token") ... p := validator.ProfilePage{&r.Form} errs = p.GetErrors() ... }

注意r.Form的型別是url.Values(即map[string][]string),同一個欄位可能對應多個值,這在設計驗證器時要格外留意(倉庫的驗證器就專門區分了單值驗證與多值驗證,詳見下文)。

容易遺漏的輸入:r.Header等隱性資料源

其它的輸入要難識別得多。例如r.Header中的很多元素是由客戶端所操縱的——User-Agent、Referer、Accept-Charset等請求頭都可以被攻擊者偽造。常常很難確認其中的哪些元素組成了輸入,所以最好的方法是把裡面所有的資料都看成是使用者輸入。即便是r.Header.Get("Accept-Charset")這樣看似由瀏覽器操縱的值,也應視為使用者輸入來對待,因為攻擊者完全可以透過自製 HTTP 請求任意改寫請求頭。

識別資料的核心結論:凡是你能夠被外部影響的值,都列入過濾清單;寧可多過濾,不可漏過濾。

過濾資料:驗證、清潔與淨化的本質

在知道資料來源之後,就可以過濾它了。「過濾」是一個有點正式的術語,它在平時表述中有很多同義詞,如驗證、清潔及淨化。儘管這些術語表面意義不同,但它們都是指同一個處理:防止非法資料進入你的應用。

過濾的正確心態:檢查而非「好心糾正」

過濾資料有很多種方法,其中有一些安全性較差。最好的方法是把過濾看成一個檢查的過程——在你使用資料之前都檢查一下,看它們是否符合合法資料的要求。而且不要試圖好心地去糾正非法資料,而要讓使用者按你制定的規則去輸入資料。

歷史證明了試圖糾正非法資料往往會導致安全漏洞。例如:「最近建設銀行系統升級之後,如果密碼後面兩位是 0,只要輸入前面四位就能登入系統」——這是一個非常嚴重的漏洞,根源正是系統為了「友好」地幫使用者補全/糾正輸入,反而放寬了校驗標準。類比到程式設計上:使用者提交了username = " astaxie "(帶空格),與其靜默幫他 Trim 後放行,不如明確告知他必須按規則輸入;如果一定要做寬鬆處理,也必須在明確定義規則的前提下進行。

三大過濾函式庫

在 Go 中,過濾資料主要採用如下一些函式庫來操作:

套件典型函式用途
strconvAtoi、ParseBool、ParseFloat、ParseInt從r.Form回傳的字串轉化成整數/浮點數,並在轉化失敗時判定非法
stringTrim、ToLower、ToTitle按照指定格式取得資訊,規範化字串內容
regexpMatchString等處理複雜需求,例如判定輸入是否是 Email、生日等格式
strconv:型別轉換即驗證

因為從 Request 中的r.Form回傳的是字串,而有些時候我們需要將之轉化成整/浮點數。Atoi、ParseBool、ParseFloat、ParseInt等函式就可以派上用場了。關鍵技巧是:型別轉換失敗本身,就是一個驗證失敗的信號。

倉庫中的驗證器 checkAge 就是這個模式的典範——先用strconv.Atoi嘗試轉換,轉換失敗即報錯,並進一步校驗數值範圍(13~130 歲):

// Check if age is a number and between 13 and 130 func checkAge(str string) error { age, err := strconv.Atoi(str) if str == "" || err != nil { return errors.New("Please enter a valid age.") } if age < 13 { return errors.New("You must be at least 13 years of age to submit.") } if age > 130 { return errors.New("You're too old to register, grandpa.") } return nil }
string:字串規範化

string套件下的Trim、ToLower、ToTitle等函式,能夠幫助我們按照指定的格式取得資訊。例如判斷使用者名稱是否為空時,先strings.Trim(str, " ")去除首尾空格再判斷,可以避免「純空格」偽裝成有效輸入——倉庫的 checkUsername 正是這樣實作的:

func checkUsername(str string) error { if strings.Trim(str, " ") == "" { return errors.New("Please enter a username.") } return nil }
regexp:複雜格式的守門員

regexp套件用來處理一些複雜的需求,例如判定輸入是否是 Email、生日之類別。倉庫中的 checkEmail 用一個簡單正則攔截不含@的偽裝地址:

func checkEmail(str string) error { if m, err := regexp.MatchString(`^[^@]+@[^@]+$`, str); !m { fmt.Println("err = ", err) return errors.New("Please enter a valid email address.") } return nil }

checkChineseName 則用 Unicode 範圍正則^[\x{4e00}-\x{9fa5}]+$確保中文名只含中文字元——這正對應了 9.2 小節「判定輸入是否符合特定格式」的應用場景。

白名單:預設非法,證明清單內才合法

過濾資料除了檢查驗證之外,在特殊時候,還可以採用白名單。即假定你正在檢查的資料都是非法的,除非能證明它是合法的。使用這個方法,如果出現錯誤,只會導致把合法的資料當成是非法的,而不會是相反——儘管我們不想犯任何錯誤,但這樣總比把非法資料當成合法資料要安全得多。

白名單思維與「糾正非法資料」形成鮮明對比:糾正法預設「輸入是對的,我幫你修」,白名單法預設「輸入是錯的,你證明給我看」。後者在安全上永遠是更保守、更穩妥的選擇。

區分過濾資料:CleanMap 防汙染注入

如果完成了上面的兩步,資料過濾的工作就基本完成了,但是在編寫 Web 應用的時候我們還需要區分已過濾和被汙染資料,因為這樣可以保證過濾資料的完整性,而不影響輸入的資料。

我們約定把所有經過過濾的資料放入一個叫全域的 Map 變數中(CleanMap)。這時需要用兩個重要的步驟來防止被汙染資料的注入:

  1. 每個請求都要初始化CleanMap為一個空 Map——避免上一次請求的殘留資料混入本次請求,造成跨請求的資料污染;
  2. 加入檢查及阻止來自外部資料來源的變數命名為CleanMap——也就是說,外部輸入(如r.Form中的欄位)永遠不能直接作為CleanMap的鍵或值進入程式,所有寫入CleanMap的資料必須先經過驗證。

簡言之,CleanMap是「已信任資料」的唯一下水道:只有通過白名單/驗證的資料才允許流入,其餘一概隔離在應用邏輯之外。這樣,程式碼的其它部分只需讀取CleanMap,就永遠不會碰到未經驗證的汙染資料。

實戰一:表單欄位的白名單過濾

接下來,讓我們透過一個例子來鞏固這些概念。請看下面這個表單——一個「我是誰」的下拉選擇框:

<form action="/whoami" method="POST"> 我是誰: <select name="name"> <option value="astaxie">astaxie</option> <option value="herry">herry</option> <option value="marry">marry</option> </select> <input type="submit" /> </form>

在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,提交name=attack這樣的資料——瀏覽器介面的下拉框限制,對攻擊者來說形同虛設,因為他根本不經過瀏覽器 UI,而是直接構造 HTTP 請求。

所以在此時我們需要做類似白名單的處理:

r.ParseForm() name := r.Form.Get("name") CleanMap := make(map[string]interface{}, 0) if name == "astaxie" || name == "herry" || name == "marry" { CleanMap["name"] = name }

上面程式碼中我們初始化了一個CleanMap的變數,當判斷取得的name是astaxie、herry、marry三個中的一個之後,我們把資料儲存到了CleanMap之中,這樣就可以確保CleanMap["name"]中的資料是合法的,從而在程式碼的其它部分使用它。

當然我們還可以在else部分增加非法資料的處理,一種可能是再次顯示錶單並提示錯誤。但是不要試圖為了友好而輸出被汙染的資料——直接把未經驗證的輸入回顯到頁面,本身就是 XSS 的常見入口(見 9.3 小節 的反射型 XSS 原理)。

與倉庫驗證器的對照:白名單的工程化寫法

這個「枚舉合法值」的思路,在倉庫 ch.4.2/validator/main.go 中被工程化為「驗證器表」:用一個map[string]func(string) error把每個表單欄位名對應到它的驗證函式,GetErrors遍歷整個表單逐欄位驗證。例如 checkGender 和 checkShirtSize 本質上就是「合法值集合」的白名單:

func checkGender(str string) error { if str == "" { return nil } siblings := []string{"m", "f", "na"} if !isElementInSlice(str, siblings) { return errors.New("Please select a valid gender.") } return nil }

這種「白名單集合 + 查表驗證」的模式比逐欄位手寫if...else更易維護、更難遺漏——新增一個欄位只需要註冊對應的驗證函式即可。

實戰二:字元集白名單(正則約束)

上面的方法對於過濾一組已知的合法值的資料很有效,但是對於過濾有一組已知合法字元組成的資料時就沒有什麼幫助。例如,你可能需要一個使用者名稱只能由字母及數字組成:

r.ParseForm() username := r.Form.Get("username") CleanMap := make(map[string]interface{}, 0) if ok, _ := regexp.MatchString("^[a-zA-Z0-9]+$", username); ok { CleanMap["username"] = username }

這就體現了兩類白名單的區別:

  • 值白名單:合法資料是一組可窮舉的離散值(如性別m/f/na、尺碼s/m/l/xl/xxl),適合用集合比對;
  • 模式白名單:合法資料滿足某種字元模式(如「僅字母與數字」的^[a-zA-Z0-9]+$),適合用正則表達式。

正則白名單同樣遵循「預設非法」原則:^...$的錨點保證整個字串都必須匹配,而不是「包含」合法字元即可——否則attack<script>這種字串也能通過[a-zA-Z0-9]的包含匹配混入系統。倉庫中 checkChineseName 的^[\x{4e00}-\x{9fa5}]+$正是同一思路的應用。

總結:把過濾當成 Web 應用的基石

資料過濾在 Web 安全中起到一個基石的作用。大多數的安全問題都是由於沒有過濾資料和驗證資料引起的,例如前面小節的 CSRF 攻擊,以及接下來將要介紹的 XSS 攻擊、SQL 注入(9.4 小節)等,都是沒有認真地過濾資料引起的,因此我們需要特別重視這部分的內容。

回顧本小節的三個核心行動準則:

  1. 識別資料:r.ParseForm()之後r.Form是最主要的輸入源;r.Header等一切可被客戶端影響的值也要視為輸入;
  2. 過濾資料:善用strconv(型別轉換即驗證)、string(Trim/ToLower 規範化)、regexp(格式白名單),並堅持「檢查而非糾正」;
  3. 區分資料:每個請求初始化空的CleanMap,只有驗證通過的資料才能寫入,外部輸入永遠不能直接污染已信任資料區。

在動手寫業務邏輯之前,先把「輸入從哪裡來、允許長什麼樣、如何進入可信區」這三件事設計清楚,你的 Go Web 應用就已經擋住了絕大多數因「誤信輸入」而產生的安全漏洞。

延伸閱讀

  • 本章總覽:9 安全與加密
  • 上一節:預防 CSRF 攻擊
  • 下一節:避免 XSS 攻擊
  • 書籍目錄:preface.md
  • 可執行的驗證器範例:ch.4.2/validator/main.go
  • 結合 token 防重複提交與輸入驗證的完整範例:ch.4.4/main.go
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

相关推荐

上一篇:Angular模块联邦终极指南:构建可扩展微前端架构的完整教程
下一篇:终极指南:Claude Code的Playwright浏览器自动化技能深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询