您现在的位置是:首页 > Golang技术干货

面向开发人员梳理的代码安全指南 - Golang代码安全指南

Golang技术干货作者:chenli日期:2021-09-09 10:42:10点击:134

Golang代码安全指南面向开发人员梳理的代码安全指南,旨在梳理API层面的风险点并提供详实可行的安全编码方案。

代码安全指引可用于以下场景:

  1. 开发人员日常参考
  2. 编写安全系统扫描策略
  3. 安全组件开发
  4. 漏洞修复指引

通用类

1. 代码实现类

1.1 内存管理

1.1.1【必须】切片长度校验

  • 在对slice进行操作时,必须判断长度是否合法,防止程序panic
  1. // bad: 未判断data的长度,可导致 index out of range
  2. func decode(data []byte) bool {
  3. if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
  4. fmt.Println("Bad")
  5. return true
  6. }
  7. return false
  8. }
  9. // bad: slice bounds out of range
  10. func foo() {
  11. var slice = []int{0, 1, 2, 3, 4, 5, 6}
  12. fmt.Println(slice[:10])
  13. }
  14. // good: 使用data前应判断长度是否合法
  15. func decode(data []byte) bool {
  16. if len(data) == 6 {
  17. if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
  18. fmt.Println("Good")
  19. return true
  20. }
  21. }
  22. return false
  23. }

1.1.2【必须】nil指针判断

  • 进行指针操作时,必须判断该指针是否为nil,防止程序panic,尤其在进行结构体Unmarshal时
  1. type Packet struct {
  2. PackeyType uint8
  3. PackeyVersion uint8
  4. Data *Data
  5. }
  6. type Data struct {
  7. Stat uint8
  8. Len uint8
  9. Buf [8]byte
  10. }
  11. func (p *Packet) UnmarshalBinary(b []byte) error {
  12. if len(b) < 2 {
  13. return io.EOF
  14. }
  15. p.PackeyType = b[0]
  16. p.PackeyVersion = b[1]
  17. // 若长度等于2,那么不会new Data
  18. if len(b) > 2 {
  19. p.Data = new(Data)
  20. }
  21. return nil
  22. }
  23. // bad: 未判断指针是否为nil
  24. func main() {
  25. packet := new(Packet)
  26. data := make([]byte, 2)
  27. if err := packet.UnmarshalBinary(data); err != nil {
  28. fmt.Println("Failed to unmarshal packet")
  29. return
  30. }
  31. fmt.Printf("Stat: %v\n", packet.Data.Stat)
  32. }
  33. // good: 判断Data指针是否为nil
  34. func main() {
  35. packet := new(Packet)
  36. data := make([]byte, 2)
  37. if err := packet.UnmarshalBinary(data); err != nil {
  38. fmt.Println("Failed to unmarshal packet")
  39. return
  40. }
  41. if packet.Data == nil {
  42. return
  43. }
  44. fmt.Printf("Stat: %v\n", packet.Data.Stat)
  45. }

1.1.3【必须】整数安全

  • 在进行数字运算操作时,需要做好长度限制,防止外部输入运算导致异常:

    • 确保无符号整数运算时不会反转
    • 确保有符号整数运算时不会出现溢出
    • 确保整型转换时不会出现截断错误
    • 确保整型转换时不会出现符号错误
  • 以下场景必须严格进行长度限制:

    • 作为数组索引
    • 作为对象的长度或者大小
    • 作为数组的边界(如作为循环计数器)
  1. // bad: 未限制长度,导致整数溢出
  2. func overflow(numControlByUser int32) {
  3. var numInt int32 = 0
  4. numInt = numControlByUser + 1
  5. // 对长度限制不当,导致整数溢出
  6. fmt.Printf("%d\n", numInt)
  7. // 使用numInt,可能导致其他错误
  8. }
  9. func main() {
  10. overflow(2147483647)
  11. }
  12. // good
  13. func overflow(numControlByUser int32) {
  14. var numInt int32 = 0
  15. numInt = numControlByUser + 1
  16. if numInt < 0 {
  17. fmt.Println("integer overflow")
  18. return
  19. }
  20. fmt.Println("integer ok")
  21. }
  22. func main() {
  23. overflow(2147483647)
  24. }

1.1.4【必须】make分配长度验证

  • 在进行make分配内存时,需要对外部可控的长度进行校验,防止程序panic。
  1. // bad
  2. func parse(lenControlByUser int, data []byte) {
  3. size := lenControlByUser
  4. // 对外部传入的size,进行长度判断以免导致panic
  5. buffer := make([]byte, size)
  6. copy(buffer, data)
  7. }
  8. // good
  9. func parse(lenControlByUser int, data []byte) ([]byte, error) {
  10. size := lenControlByUser
  11. // 限制外部可控的长度大小范围
  12. if size > 64*1024*1024 {
  13. return nil, errors.New("value too large")
  14. }
  15. buffer := make([]byte, size)
  16. copy(buffer, data)
  17. return buffer, nil
  18. }

1.1.5【必须】禁止SetFinalizer和指针循环引用同时使用

  • 当一个对象从被GC选中到移除内存之前,runtime.SetFinalizer()都不会执行,即使程序正常结束或者发生错误。由指针构成的“循环引用”虽然能被GC正确处理,但由于无法确定Finalizer依赖顺序,从而无法调用runtime.SetFinalizer(),导致目标对象无法变成可达状态,从而造成内存无法被回收。
  1. // bad
  2. func foo() {
  3. var a, b Data
  4. a.o = &b
  5. b.o = &a
  6. // 指针循环引用,SetFinalizer()无法正常调用
  7. runtime.SetFinalizer(&a, func(d *Data) {
  8. fmt.Printf("a %p final.\n", d)
  9. })
  10. runtime.SetFinalizer(&b, func(d *Data) {
  11. fmt.Printf("b %p final.\n", d)
  12. })
  13. }
  14. func main() {
  15. for {
  16. foo()
  17. time.Sleep(time.Millisecond)
  18. }
  19. }

1.1.6【必须】禁止重复释放channel

  • 重复释放一般存在于异常流程判断中,如果恶意攻击者构造出异常条件使程序重复释放channel,则会触发运行时panic,从而造成DoS攻击。
  1. // bad
  2. func foo(c chan int) {
  3. defer close(c)
  4. err := processBusiness()
  5. if err != nil {
  6. c <- 0
  7. close(c) // 重复释放channel
  8. return
  9. }
  10. c <- 1
  11. }
  12. // good
  13. func foo(c chan int) {
  14. defer close(c) // 使用defer延迟关闭channel
  15. err := processBusiness()
  16. if err != nil {
  17. c <- 0
  18. return
  19. }
  20. c <- 1
  21. }

1.1.7【必须】确保每个协程都能退出

  • 启动一个协程就会做一个入栈操作,在系统不退出的情况下,协程也没有设置退出条件,则相当于协程失去了控制,它占用的资源无法回收,可能会导致内存泄露。
  1. // bad: 协程没有设置退出条件
  2. func doWaiter(name string, second int) {
  3. for {
  4. time.Sleep(time.Duration(second) * time.Second)
  5. fmt.Println(name, " is ready!")
  6. }
  7. }

1.1.8【推荐】不使用unsafe包

  • 由于unsafe包绕过了 Golang 的内存安全原则,一般来说使用该库是不安全的,可导致内存破坏,尽量避免使用该包。若必须要使用unsafe操作指针,必须做好安全校验。
  1. // bad: 通过unsafe操作原始指针
  2. func unsafePointer() {
  3. b := make([]byte, 1)
  4. foo := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(0xfffffffe)))
  5. fmt.Print(*foo + 1)
  6. }
  7. // [signal SIGSEGV: segmentation violation code=0x1 addr=0xc100068f55 pc=0x49142b]

1.1.9【推荐】不使用slice作为函数入参

  • slice是引用类型,在作为函数入参时采用的是地址传递,对slice的修改也会影响原始数据
  1. // bad: slice作为函数入参时是地址传递
  2. func modify(array []int) {
  3. array[0] = 10 // 对入参slice的元素修改会影响原始数据
  4. }
  5. func main() {
  6. array := []int{1, 2, 3, 4, 5}
  7. modify(array)
  8. fmt.Println(array) // output:[10 2 3 4 5]
  9. }
  10. // good: 函数使用数组作为入参,而不是slice
  11. func modify(array [5]int) {
  12. array[0] = 10
  13. }
  14. func main() {
  15. // 传入数组,注意数组与slice的区别
  16. array := [5]int{1, 2, 3, 4, 5}
  17. modify(array)
  18. fmt.Println(array)
  19. }

<a id="1.1.2"></a>

1.2 文件操作

1.2.1【必须】 路径穿越检查

  • 在进行文件操作时,如果对外部传入的文件名未做限制,可能导致任意文件读取或者任意文件写入,严重可能导致代码执行。
  1. // bad: 任意文件读取
  2. func handler(w http.ResponseWriter, r *http.Request) {
  3. path := r.URL.Query()["path"][0]
  4. // 未过滤文件路径,可能导致任意文件读取
  5. data, _ := ioutil.ReadFile(path)
  6. w.Write(data)
  7. // 对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
  8. data, _ = ioutil.ReadFile(filepath.Join("/home/user/", path))
  9. w.Write(data)
  10. }
  11. // bad: 任意文件写入
  12. func unzip(f string) {
  13. r, _ := zip.OpenReader(f)
  14. for _, f := range r.File {
  15. p, _ := filepath.Abs(f.Name)
  16. // 未验证压缩文件名,可能导致../等路径穿越,任意文件路径写入
  17. ioutil.WriteFile(p, []byte("present"), 0640)
  18. }
  19. }
  20. // good: 检查压缩的文件名是否包含..路径穿越特征字符,防止任意写入
  21. func unzipGood(f string) bool {
  22. r, err := zip.OpenReader(f)
  23. if err != nil {
  24. fmt.Println("read zip file fail")
  25. return false
  26. }
  27. for _, f := range r.File {
  28. if !strings.Contains(f.Name, "..") {
  29. p, _ := filepath.Abs(f.Name)
  30. ioutil.WriteFile(p, []byte("present"), 0640)
  31. } else {
  32. return false
  33. }
  34. }
  35. return true
  36. }

1.2.2【必须】 文件访问权限

  • 根据创建文件的敏感性设置不同级别的访问权限,以防止敏感数据被任意权限用户读取。例如,设置文件权限为:-rw-r-----
  1. ioutil.WriteFile(p, []byte("present"), 0640)

<a id="1.1.3"></a>

1.3 系统接口

1.3.1【必须】命令执行检查

  • 使用exec.Commandexec.CommandContextsyscall.StartProcessos.StartProcess等函数时,第一个参数(path)直接取外部输入值时,应使用白名单限定可执行的命令范围,不允许传入bashcmdsh等命令;
  • 使用exec.Commandexec.CommandContext等函数时,通过bashcmdsh等创建shell,-c后的参数(arg)拼接外部输入,应过滤\n $ & ; | ‘ “ ( ) `等潜在恶意字符;
  1. // bad
  2. func foo() {
  3. userInputedVal := "&& echo 'hello'" // 假设外部传入该变量值
  4. cmdName := "ping " + userInputedVal
  5. // 未判断外部输入是否存在命令注入字符,结合sh可造成命令注入
  6. cmd := exec.Command("sh", "-c", cmdName)
  7. output, _ := cmd.CombinedOutput()
  8. fmt.Println(string(output))
  9. cmdName := "ls"
  10. // 未判断外部输入是否是预期命令
  11. cmd := exec.Command(cmdName)
  12. output, _ := cmd.CombinedOutput()
  13. fmt.Println(string(output))
  14. }
  15. // good
  16. func checkIllegal(cmdName string) bool {
  17. if strings.Contains(cmdName, "&") || strings.Contains(cmdName, "|") || strings.Contains(cmdName, ";") ||
  18. strings.Contains(cmdName, "$") || strings.Contains(cmdName, "'") || strings.Contains(cmdName, "`") ||
  19. strings.Contains(cmdName, "(") || strings.Contains(cmdName, ")") || strings.Contains(cmdName, "\"") {
  20. return true
  21. }
  22. return false
  23. }
  24. func main() {
  25. userInputedVal := "&& echo 'hello'"
  26. cmdName := "ping " + userInputedVal
  27. if checkIllegal(cmdName) { // 检查传给sh的命令是否有特殊字符
  28. return // 存在特殊字符直接return
  29. }
  30. cmd := exec.Command("sh", "-c", cmdName)
  31. output, _ := cmd.CombinedOutput()
  32. fmt.Println(string(output))
  33. }

<a id="1.1.4"></a>

1.4 通信安全

1.4.1【必须】网络通信采用TLS方式

  • 明文传输的通信协议目前已被验证存在较大安全风险,被中间人劫持后可能导致许多安全风险,因此必须采用至少TLS的安全通信方式保证通信安全,例如gRPC/Websocket都使用TLS1.3。
  1. // good
  2. func main() {
  3. http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
  4. w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
  5. w.Write([]byte("This is an example server.\n"))
  6. })
  7. // 服务器配置证书与私钥
  8. log.Fatal(http.ListenAndServeTLS(":443", "yourCert.pem", "yourKey.pem", nil))
  9. }

1.4.2【推荐】TLS启用证书验证

  • TLS证书应当是有效的、未过期的,且配置正确的域名,生产环境的服务端应启用证书验证。
  1. // bad
  2. import (
  3. "crypto/tls"
  4. "net/http"
  5. )
  6. func doAuthReq(authReq *http.Request) *http.Response {
  7. tr := &http.Transport{
  8. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  9. }
  10. client := &http.Client{Transport: tr}
  11. res, _ := client.Do(authReq)
  12. return res
  13. }
  14. // good
  15. import (
  16. "crypto/tls"
  17. "net/http"
  18. )
  19. func doAuthReq(authReq *http.Request) *http.Response {
  20. tr := &http.Transport{
  21. TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
  22. }
  23. client := &http.Client{Transport: tr}
  24. res, _ := client.Do(authReq)
  25. return res
  26. }

<a id="1.1.5"></a>

1.5 敏感数据保护

1.5.1【必须】敏感信息访问

  • 禁止将敏感信息硬编码在程序中,既可能会将敏感信息暴露给攻击者,也会增加代码管理和维护的难度
  • 使用配置中心系统统一托管密钥等敏感信息

1.5.2【必须】敏感数据输出

  • 只输出必要的最小数据集,避免多余字段暴露引起敏感信息泄露
  • 不能在日志保存密码(包括明文密码和密文密码)、密钥和其它敏感信息
  • 对于必须输出的敏感信息,必须进行合理脱敏展示
  1. // bad
  2. func serve() {
  3. http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
  4. r.ParseForm()
  5. user := r.Form.Get("user")
  6. pw := r.Form.Get("password")
  7. log.Printf("Registering new user %s with password %s.\n", user, pw)
  8. })
  9. http.ListenAndServe(":80", nil)
  10. }
  11. // good
  12. func serve1() {
  13. http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
  14. r.ParseForm()
  15. user := r.Form.Get("user")
  16. pw := r.Form.Get("password")
  17. log.Printf("Registering new user %s.\n", user)
  18. // ...
  19. use(pw)
  20. })
  21. http.ListenAndServe(":80", nil)
  22. }
  • 避免通过GET方法、代码注释、自动填充、缓存等方式泄露敏感信息

1.5.3【必须】敏感数据存储

  • 敏感数据应使用SHA2、RSA等算法进行加密存储
  • 敏感数据应使用独立的存储层,并在访问层开启访问控制
  • 包含敏感信息的临时文件或缓存一旦不再需要应立刻删除

1.5.4【必须】异常处理和日志记录

  • 应合理使用panic、recover、defer处理系统异常,避免出错信息输出到前端
  1. defer func () {
  2. if r := recover(); r != nil {
  3. fmt.Println("Recovered in start()")
  4. }
  5. }()
  • 对外环境禁止开启debug模式,或将程序运行日志输出到前端
  1. // bad
  2. dlv --listen=:2345 --headless=true --api-version=2 debug test.go
  3. // good
  4. dlv debug test.go

<a id="1.1.6"></a>

1.6 加密解密

1.6.1【必须】不得硬编码密码/密钥

  • 在进行用户登陆,加解密算法等操作时,不得在代码里硬编码密钥或密码,可通过变换算法或者配置等方式设置密码或者密钥。
  1. // bad
  2. const (
  3. user = "dbuser"
  4. password = "s3cretp4ssword"
  5. )
  6. func connect() *sql.DB {
  7. connStr := fmt.Sprintf("postgres://%s:%s@localhost/pqgotest", user, password)
  8. db, err := sql.Open("postgres", connStr)
  9. if err != nil {
  10. return nil
  11. }
  12. return db
  13. }
  14. // bad
  15. var (
  16. commonkey = []byte("0123456789abcdef")
  17. )
  18. func AesEncrypt(plaintext string) (string, error) {
  19. block, err := aes.NewCipher(commonkey)
  20. if err != nil {
  21. return "", err
  22. }
  23. }

1.6.2【必须】密钥存储安全

  • 在使用对称密码算法时,需要保护好加密密钥。当算法涉及敏感、业务数据时,可通过非对称算法协商加密密钥。其他较为不敏感的数据加密,可以通过变换算法等方式保护密钥。

1.6.3【推荐】不使用弱密码算法

  • 在使用加密算法时,不建议使用加密强度较弱的算法。
  1. // bad
  2. crypto/descrypto/md5crypto/sha1crypto/rc4等。
  3. // good
  4. crypto/rsacrypto/aes等。

<a id="1.1.7"></a>

1.7 正则表达式

1.7.1【推荐】使用regexp进行正则表达式匹配

  • 正则表达式编写不恰当可被用于DoS攻击,造成服务不可用,推荐使用regexp包进行正则表达式匹配。regexp保证了线性时间性能和优雅的失败:对解析器、编译器和执行引擎都进行了内存限制。但regexp不支持以下正则表达式特性,如业务依赖这些特性,则regexp不适合使用。
  1. // good
  2. matched, err := regexp.MatchString(`a.b`, "aaxbb")
  3. fmt.Println(matched) // true
  4. fmt.Println(err) // nil

<a id="2"></a>

后台类

<a id="2.1"></a>

1 代码实现类

<a id="2.1.1"></a>

1.1 输入校验

1.1.1【必须】按类型进行数据校验

  • 所有外部输入的参数,应使用validator进行白名单校验,校验内容包括但不限于数据长度、数据范围、数据类型与格式,校验不通过的应当拒绝
  1. // good
  2. import (
  3. "fmt"
  4. "github.com/go-playground/validator/v10"
  5. )
  6. var validate *validator.Validate
  7. func validateVariable() {
  8. myEmail := "abc@tencent.com"
  9. errs := validate.Var(myEmail, "required,email")
  10. if errs != nil {
  11. fmt.Println(errs)
  12. return
  13. //停止执行
  14. }
  15. // 验证通过,继续执行
  16. ...
  17. }
  18. func main() {
  19. validate = validator.New()
  20. validateVariable()
  21. }
  • 无法通过白名单校验的应使用html.EscapeStringtext/templatebluemonday<, >, &, ',"等字符进行过滤或编码
  1. import (
  2. "text/template"
  3. )
  4. // TestHTMLEscapeString HTML特殊字符转义
  5. func main(inputValue string) string {
  6. escapedResult := template.HTMLEscapeString(inputValue)
  7. return escapedResult
  8. }

<a id="2.1.2"></a>

1.2 SQL操作

1.2.1【必须】SQL语句默认使用预编译并绑定变量

  • 使用database/sql的prepare、Query或使用GORM等ORM执行SQL操作
  1. import (
  2. "github.com/jinzhu/gorm"
  3. _ "github.com/jinzhu/gorm/dialects/sqlite"
  4. )
  5. type Product struct {
  6. gorm.Model
  7. Code string
  8. Price uint
  9. }
  10. ...
  11. var product Product
  12. ...
  13. db.First(&product, 1)
  • 使用参数化查询,禁止拼接SQL语句,另外对于传入参数用于order by或表名的需要通过校验
  1. // bad
  2. import (
  3. "database/sql"
  4. "fmt"
  5. "net/http"
  6. )
  7. func handler(db *sql.DB, req *http.Request) {
  8. q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE",
  9. req.URL.Query()["category"])
  10. db.Query(q)
  11. }
  12. // good
  13. func handlerGood(db *sql.DB, req *http.Request) {
  14. // 使用?占位符
  15. q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE"
  16. db.Query(q, req.URL.Query()["category"])
  17. }

<a id="2.1.3"></a>

1.3 网络请求

1.3.1【必须】资源请求过滤验证

  • 使用"net/http"下的方法http.Get(url)http.Post(url, contentType, body)http.Head(url)http.PostForm(url, data)http.Do(req)时,如变量值外部可控(指从参数中动态获取),应对请求目标进行严格的安全校验。

  • 如请求资源域名归属固定的范围,如只允许a.qq.comb.qq.com,应做白名单限制。如不适用白名单,则推荐的校验逻辑步骤是:

    • 第 1 步、只允许HTTP或HTTPS协议

    • 第 2 步、解析目标URL,获取其HOST

    • 第 3 步、解析HOST,获取HOST指向的IP地址转换成Long型

    • 第 4 步、检查IP地址是否为内网IP,网段有:

      1. // 以RFC定义的专有网络为例,如有自定义私有网段亦应加入禁止访问列表。
      2. 10.0.0.0/8
      3. 172.16.0.0/12
      4. 192.168.0.0/16
      5. 127.0.0.0/8
    • 第 5 步、请求URL

    • 第 6 步、如有跳转,跳转后执行1,否则绑定经校验的ip和域名,对URL发起请求

  • 官方库encoding/xml不支持外部实体引用,使用该库可避免xxe漏洞

  1. import (
  2. "encoding/xml"
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. type Person struct {
  8. XMLName xml.Name `xml:"person"`
  9. Id int `xml:"id,attr"`
  10. UserName string `xml:"name>first"`
  11. Comment string `xml:",comment"`
  12. }
  13. v := &Person{Id: 13, UserName: "John"}
  14. v.Comment = " Need more details. "
  15. enc := xml.NewEncoder(os.Stdout)
  16. enc.Indent(" ", " ")
  17. if err := enc.Encode(v); err != nil {
  18. fmt.Printf("error: %v\n", err)
  19. }
  20. }

<a id="2.1.4"></a>

1.4 服务器端渲染

1.4.1【必须】模板渲染过滤验证

  • 使用text/template或者html/template渲染模板时禁止将外部输入参数引入模板,或仅允许引入白名单内字符。
  1. // bad
  2. func handler(w http.ResponseWriter, r *http.Request) {
  3. r.ParseForm()
  4. x := r.Form.Get("name")
  5. var tmpl = `<!DOCTYPE html><html><body>
  6. <form action="/" method="post">
  7. First name:<br>
  8. <input type="text" name="name" value="">
  9. <input type="submit" value="Submit">
  10. </form><p>` + x + ` </p></body></html>`
  11. t := template.New("main")
  12. t, _ = t.Parse(tmpl)
  13. t.Execute(w, "Hello")
  14. }
  15. // good
  16. import (
  17. "fmt"
  18. "github.com/go-playground/validator/v10"
  19. )
  20. var validate *validator.Validate
  21. validate = validator.New()
  22. func validateVariable(val) {
  23. errs := validate.Var(val, "gte=1,lte=100") // 限制必须是1-100的正整数
  24. if errs != nil {
  25. fmt.Println(errs)
  26. return false
  27. }
  28. return true
  29. }
  30. func handler(w http.ResponseWriter, r *http.Request) {
  31. r.ParseForm()
  32. x := r.Form.Get("name")
  33. if validateVariable(x) {
  34. var tmpl = `<!DOCTYPE html><html><body>
  35. <form action="/" method="post">
  36. First name:<br>
  37. <input type="text" name="name" value="">
  38. <input type="submit" value="Submit">
  39. </form><p>` + x + ` </p></body></html>`
  40. t := template.New("main")
  41. t, _ = t.Parse(tmpl)
  42. t.Execute(w, "Hello")
  43. } else {
  44. // ...
  45. }
  46. }

<a id="2.1.5"></a>

1.5 Web跨域

1.5.1【必须】跨域资源共享CORS限制请求来源

  • CORS请求保护不当可导致敏感信息泄漏,因此应当严格设置Access-Control-Allow-Origin使用同源策略进行保护。
  1. // good
  2. c := cors.New(cors.Options{
  3. AllowedOrigins: []string{"http://qq.com", "https://qq.com"},
  4. AllowCredentials: true,
  5. Debug: false,
  6. })
  7. // 引入中间件
  8. handler = c.Handler(handler)

<a id="2.1.6"></a>

1.6 响应输出

1.6.1 【必须】设置正确的HTTP响应包类型

  • 响应头Content-Type与实际响应内容,应保持一致。如:API响应数据类型是json,则响应头使用application/json;若为xml,则设置为text/xml

1.6.2 【必须】添加安全响应头

  • 所有接口、页面,添加响应头 X-Content-Type-Options: nosniff
  • 所有接口、页面,添加响应头X-Frame-Options。按需合理设置其允许范围,包括:DENYSAMEORIGINALLOW-FROM origin。用法参考:MDN文档

1.6.3【必须】外部输入拼接到HTTP响应头中需进行过滤

  • 应尽量避免外部可控参数拼接到HTTP响应头中,如业务需要则需要过滤掉\r\n等换行符,或者拒绝携带换行符号的外部输入。

1.6.4【必须】外部输入拼接到response页面前进行编码处理

  • 直出html页面或使用模板生成html页面的,推荐使用text/template自动编码,或者使用html.EscapeStringtext/template<, >, &, ',"等字符进行编码。
  1. import (
  2. "html/template"
  3. )
  4. func outtemplate(w http.ResponseWriter, r *http.Request) {
  5. param1 := r.URL.Query().Get("param1")
  6. tmpl := template.New("hello")
  7. tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
  8. tmpl.ExecuteTemplate(w, "T", param1)
  9. }

<a id="2.1.7"></a>

1.7 会话管理

1.7.1【必须】安全维护session信息

  • 用户登录时应重新生成session,退出登录后应清理session。
    ```go
    import (
    “github.com/gorilla/handlers”
    “github.com/gorilla/mux”
    “net/http”
    )

// 创建cookie
func setToken(res http.ResponseWriter, req http.Request) {
expireToken := time.Now().Add(time.Minute
30).Unix()
expireCookie := time.Now().Add(time.Minute * 30)

  1. //...
  2. cookie := http.Cookie{
  3. Name: "Auth",
  4. Value: signedToken,
  5. Expires: expireCookie, // 过期失效
  6. HttpOnly: true,
  7. Path: "/",
  8. Domain: "127.0.0.1",
  9. Secure: true,
  10. }
  11. http.SetCookie(res, &cookie)
  12. http.Redirect(res, req, "/profile", 307)

}

// 删除cookie
func logout(res http.ResponseWriter, req *http.Request) {
deleteCookie := http.Cookie{
Name: “Auth”,
Value: “none”,
Expires: time.Now(),
}
http.SetCookie(res, &deleteCookie)
return
}

  1. #### 1.7.2【必须】CSRF防护
  2. - 涉及系统敏感操作或可读取敏感信息的接口应校验`Referer`或添加`csrf_token`
  3. ```go
  4. // good
  5. import (
  6. "github.com/gorilla/csrf"
  7. "github.com/gorilla/mux"
  8. "net/http"
  9. )
  10. func main() {
  11. r := mux.NewRouter()
  12. r.HandleFunc("/signup", ShowSignupForm)
  13. r.HandleFunc("/signup/post", SubmitSignupForm)
  14. // 使用csrf_token验证
  15. http.ListenAndServe(":8000",
  16. csrf.Protect([]byte("32-byte-long-auth-key"))(r))
  17. }

<a id="2.1.8"></a>

1.8 访问控制

1.8.1【必须】默认鉴权

  • 除非资源完全可对外开放,否则系统默认进行身份认证,使用白名单的方式放开不需要认证的接口或页面。

  • 根据资源的机密程度和用户角色,以最小权限原则,设置不同级别的权限,如完全公开、登录可读、登录可写、特定用户可读、特定用户可写等

  • 涉及用户自身相关的数据的读写必须验证登录态用户身份及其权限,避免越权操作

    1. -- 伪代码
    2. select id from table where id=:id and userid=session.userid
  • 没有独立账号体系的外网服务使用QQ微信登录,内网服务使用统一登录服务登录,其他使用账号密码登录的服务需要增加验证码等二次验证

<a id="2.1.9"></a>

1.9 并发保护

1.9.1【必须】禁止在闭包中直接调用循环变量

  • 在循环中启动协程,当协程中使用到了循环的索引值,由于多个协程同时使用同一个变量会产生数据竞争,造成执行结果异常。
  1. // bad
  2. func main() {
  3. runtime.GOMAXPROCS(runtime.NumCPU())
  4. var group sync.WaitGroup
  5. for i := 0; i < 5; i++ {
  6. group.Add(1)
  7. go func() {
  8. defer group.Done()
  9. fmt.Printf("%-2d", i) // 这里打印的i不是所期望的
  10. }()
  11. }
  12. group.Wait()
  13. }
  14. // good
  15. func main() {
  16. runtime.GOMAXPROCS(runtime.NumCPU())
  17. var group sync.WaitGroup
  18. for i := 0; i < 5; i++ {
  19. group.Add(1)
  20. go func(j int) {
  21. defer func() {
  22. if r := recover(); r != nil {
  23. fmt.Println("Recovered in start()")
  24. }
  25. group.Done()
  26. }()
  27. fmt.Printf("%-2d", j) // 闭包内部使用局部变量
  28. }(i) // 把循环变量显式地传给协程
  29. }
  30. group.Wait()
  31. }

1.9.2【必须】禁止并发写map

  • 并发写map容易造成程序崩溃并异常退出,建议加锁保护
    1. // bad
    2. func main() {
    3. m := make(map[int]int)
    4. // 并发读写
    5. go func() {
    6. for {
    7. _ = m[1]
    8. }
    9. }()
    10. go func() {
    11. for {
    12. m[2] = 1
    13. }
    14. }()
    15. select {}
    16. }

    1.9.3【必须】确保并发安全

敏感操作如果未作并发安全限制,可导致数据读写异常,造成业务逻辑限制被绕过。可通过同步锁或者原子操作进行防护。

通过同步锁共享内存

  1. // good
  2. var count int
  3. func Count(lock *sync.Mutex) {
  4. lock.Lock() // 加写锁
  5. count++
  6. fmt.Println(count)
  7. lock.Unlock() // 解写锁,任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()
  8. }
  9. func main() {
  10. lock := &sync.Mutex{}
  11. for i := 0; i < 10; i++ {
  12. go Count(lock) // 传递指针是为了防止函数内的锁和调用锁不一致
  13. }
  14. for {
  15. lock.Lock()
  16. c := count
  17. lock.Unlock()
  18. runtime.Gosched() // 交出时间片给协程
  19. if c > 10 {
  20. break
  21. }
  22. }
  23. }
  • 使用sync/atomic执行原子操作
  1. // good
  2. import (
  3. "sync"
  4. "sync/atomic"
  5. )
  6. func main() {
  7. type Map map[string]string
  8. var m atomic.Value
  9. m.Store(make(Map))
  10. var mu sync.Mutex // used only by writers
  11. read := func(key string) (val string) {
  12. m1 := m.Load().(Map)
  13. return m1[key]
  14. }
  15. insert := func(key, val string) {
  16. mu.Lock() // 与潜在写入同步
  17. defer mu.Unlock()
  18. m1 := m.Load().(Map) // 导入struct当前数据
  19. m2 := make(Map) // 创建新值
  20. for k, v := range m1 {
  21. m2[k] = v
  22. }
  23. m2[key] = val
  24. m.Store(m2) // 用新的替代当前对象
  25. }
  26. _, _ = read, insert
  27. }

文章评论