«
Go安全指南

时间:2021-9-26    作者:smarteng    分类: Go语言


目录 - [1 通用类](#1) * [I. 代码实现](#1.1) + [1.1 内存管理](#1.1.1) + [1.2 文件操作](#1.1.2) + [1.3 系统接口](#1.1.3) + [1.4 通信安全](#1.1.4) + [1.5 敏感数据保护](#1.1.5) + [1.6 加密解密](#1.1.6) + [1.7 正则表达式](#1.1.7) - [2 后台类](#2) * [I. 代码实现](#2.1) + [1.1 输入校验](#2.1.1) + [1.2 SQL操作](#2.1.2) + [1.3 网络请求](#2.1.3) + [1.4 服务器端渲染](#2.1.4) + [1.5 Web跨域](#2.1.5) + [1.6 响应输出](#2.1.6) + [1.7 会话管理](#2.1.7) + [1.8 访问控制](#2.1.8) + [1.9 并发保护](#2.1.9)

通用类

1. 代码实现类

1.1 内存管理

1.1.1【必须】切片长度校验

// bad: 未判断data的长度,可导致 index out of range
func decode(data []byte) bool {
    if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
        fmt.Println("Bad")
        return true
    }
    return false
}

// bad: slice bounds out of range
func foo() {
    var slice = []int{0, 1, 2, 3, 4, 5, 6}
    fmt.Println(slice[:10])
}

// good: 使用data前应判断长度是否合法
func decode(data []byte) bool {
    if len(data) == 6 {
        if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
            fmt.Println("Good")
            return true
        }
    }
    return false
}

1.1.2【必须】nil指针判断

type Packet struct {
    PackeyType    uint8
    PackeyVersion uint8
    Data          *Data
}

type Data struct {
    Stat uint8
    Len  uint8
    Buf  [8]byte
}

func (p *Packet) UnmarshalBinary(b []byte) error {
    if len(b) < 2 {
        return io.EOF
    }

    p.PackeyType = b[0]
    p.PackeyVersion = b[1]

    // 若长度等于2,那么不会new Data
    if len(b) > 2 {
        p.Data = new(Data)
    }
    return nil
}

// bad: 未判断指针是否为nil
func main() {
    packet := new(Packet)
    data := make([]byte, 2)
    if err := packet.UnmarshalBinary(data); err != nil {
        fmt.Println("Failed to unmarshal packet")
        return
    }

    fmt.Printf("Stat: %v\n", packet.Data.Stat)
}

// good: 判断Data指针是否为nil
func main() {
    packet := new(Packet)
    data := make([]byte, 2)

    if err := packet.UnmarshalBinary(data); err != nil {
        fmt.Println("Failed to unmarshal packet")
        return
    }

    if packet.Data == nil {
        return
    }

    fmt.Printf("Stat: %v\n", packet.Data.Stat)
}

1.1.3【必须】整数安全

// bad: 未限制长度,导致整数溢出
func overflow(numControlByUser int32) {
    var numInt int32 = 0
    numInt = numControlByUser + 1
    // 对长度限制不当,导致整数溢出
    fmt.Printf("%d\n", numInt)
    // 使用numInt,可能导致其他错误
}

func main() {
    overflow(2147483647)
}

// good
func overflow(numControlByUser int32) {
    var numInt int32 = 0
    numInt = numControlByUser + 1
    if numInt < 0 {
        fmt.Println("integer overflow")
        return
    }
    fmt.Println("integer ok")
}

func main() {
    overflow(2147483647)
}

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

// bad
func parse(lenControlByUser int, data []byte) {
    size := lenControlByUser
    // 对外部传入的size,进行长度判断以免导致panic
    buffer := make([]byte, size)
    copy(buffer, data)
}

// good
func parse(lenControlByUser int, data []byte) ([]byte, error) {
    size := lenControlByUser
    // 限制外部可控的长度大小范围
    if size > 64*1024*1024 {
        return nil, errors.New("value too large")
    }
    buffer := make([]byte, size)
    copy(buffer, data)
    return buffer, nil
}

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

// bad
func foo() {
    var a, b Data
    a.o = &b
    b.o = &a

    // 指针循环引用,SetFinalizer()无法正常调用
    runtime.SetFinalizer(&a, func(d *Data) {
        fmt.Printf("a %p final.\n", d)
    })
    runtime.SetFinalizer(&b, func(d *Data) {
        fmt.Printf("b %p final.\n", d)
    })
}

func main() {
    for {
        foo()
        time.Sleep(time.Millisecond)
    }
}

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

// bad
func foo(c chan int) {
    defer close(c)
    err := processBusiness()
    if err != nil {
        c <- 0
        close(c) // 重复释放channel
        return
    }
    c <- 1
}

// good
func foo(c chan int) {
    defer close(c) // 使用defer延迟关闭channel
    err := processBusiness()
    if err != nil {
        c <- 0
        return
    }
    c <- 1
}

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

// bad: 协程没有设置退出条件
func doWaiter(name string, second int) {
    for {
        time.Sleep(time.Duration(second) * time.Second)
        fmt.Println(name, " is ready!")
    }
}

1.1.8【推荐】不使用unsafe包

// bad: 通过unsafe操作原始指针
func unsafePointer() {
    b := make([]byte, 1)
    foo := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(0xfffffffe)))
    fmt.Print(*foo + 1)
}

// [signal SIGSEGV: segmentation violation code=0x1 addr=0xc100068f55 pc=0x49142b]

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

  // bad
  // slice作为函数入参时包含原始数组指针
  func modify(array []int) {
      array[0] = 10 // 对入参slice的元素修改会影响原始数据
  }

  func main() {
      array := []int{1, 2, 3, 4, 5}

      modify(array)
      fmt.Println(array) // output:[10 2 3 4 5]
  }

  // good
  // 数组作为函数入参,而不是slice
  func modify(array [5]int) {
    array[0] = 10
  }

  func main() {
      // 传入数组,注意数组与slice的区别
      array := [5]int{1, 2, 3, 4, 5}

      modify(array)
      fmt.Println(array)
  }

1.2 文件操作

1.2.1【必须】 路径穿越检查

// bad: 任意文件读取
func handler(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Query()["path"][0]

    // 未过滤文件路径,可能导致任意文件读取
    data, _ := ioutil.ReadFile(path)
    w.Write(data)

    // 对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
    data, _ = ioutil.ReadFile(filepath.Join("/home/user/", path))
    w.Write(data)
}

// bad: 任意文件写入
func unzip(f string) {
    r, _ := zip.OpenReader(f)
    for _, f := range r.File {
        p, _ := filepath.Abs(f.Name)
        // 未验证压缩文件名,可能导致../等路径穿越,任意文件路径写入
        ioutil.WriteFile(p, []byte("present"), 0640)
    }
}

// good: 检查压缩的文件名是否包含..路径穿越特征字符,防止任意写入
func unzipGood(f string) bool {
    r, err := zip.OpenReader(f)
    if err != nil {
        fmt.Println("read zip file fail")
        return false
    }
    for _, f := range r.File {
        if !strings.Contains(f.Name, "..") {
            p, _ := filepath.Abs(f.Name)
            ioutil.WriteFile(p, []byte("present"), 0640)
        } else {
            return false
        }
    }
    return true
}

1.2.2【必须】 文件访问权限

ioutil.WriteFile(p, []byte("present"), 0640)

1.3 系统接口

1.3.1【必须】命令执行检查

// bad
func foo() {
    userInputedVal := "&& echo 'hello'" // 假设外部传入该变量值
    cmdName := "ping " + userInputedVal

    // 未判断外部输入是否存在命令注入字符,结合sh可造成命令注入
    cmd := exec.Command("sh", "-c", cmdName)
    output, _ := cmd.CombinedOutput()
    fmt.Println(string(output))

    cmdName := "ls"
    // 未判断外部输入是否是预期命令
    cmd := exec.Command(cmdName)
    output, _ := cmd.CombinedOutput()
    fmt.Println(string(output))
}

// good
func checkIllegal(cmdName string) bool {
    if strings.Contains(cmdName, "&") || strings.Contains(cmdName, "|") || strings.Contains(cmdName, ";") ||
        strings.Contains(cmdName, "$") || strings.Contains(cmdName, "'") || strings.Contains(cmdName, "`") ||
        strings.Contains(cmdName, "(") || strings.Contains(cmdName, ")") || strings.Contains(cmdName, "\"") {
        return true
    }
    return false
}

func main() {
    userInputedVal := "&& echo 'hello'"
    cmdName := "ping " + userInputedVal

    if checkIllegal(cmdName) { // 检查传给sh的命令是否有特殊字符
        return // 存在特殊字符直接return
    }

    cmd := exec.Command("sh", "-c", cmdName)
    output, _ := cmd.CombinedOutput()
    fmt.Println(string(output))
}

1.4 通信安全

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

// good
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
        w.Write([]byte("This is an example server.\n"))
    })

    // 服务器配置证书与私钥
    log.Fatal(http.ListenAndServeTLS(":443", "yourCert.pem", "yourKey.pem", nil))
}

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

// bad
import (
    "crypto/tls"
    "net/http"
)

func doAuthReq(authReq *http.Request) *http.Response {
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}
    res, _ := client.Do(authReq)
    return res
}

// good
import (
    "crypto/tls"
    "net/http"
)

func doAuthReq(authReq *http.Request) *http.Response {
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
    }
    client := &http.Client{Transport: tr}
    res, _ := client.Do(authReq)
    return res
}

1.5 敏感数据保护

1.5.1【必须】敏感信息访问

1.5.2【必须】敏感数据输出

// bad
func serve() {
    http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        user := r.Form.Get("user")
        pw := r.Form.Get("password")

        log.Printf("Registering new user %s with password %s.\n", user, pw)
    })
    http.ListenAndServe(":80", nil)
}

// good
func serve1() {
    http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        user := r.Form.Get("user")
        pw := r.Form.Get("password")

        log.Printf("Registering new user %s.\n", user)

        // ...
        use(pw)
    })
    http.ListenAndServe(":80", nil)
}

1.5.3【必须】敏感数据存储

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

defer func () {
    if r := recover(); r != nil {
        fmt.Println("Recovered in start()")
    }
}()
// bad
dlv --listen=:2345 --headless=true --api-version=2 debug test.go
// good
dlv debug test.go

1.6 加密解密

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

// bad
const (
    user     = "dbuser"
    password = "s3cretp4ssword"
)

func connect() *sql.DB {
    connStr := fmt.Sprintf("postgres://%s:%s@localhost/pqgotest", user, password)
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil
    }
    return db
}

// bad
var (
    commonkey = []byte("0123456789abcdef")
)

func AesEncrypt(plaintext string) (string, error) {
    block, err := aes.NewCipher(commonkey)
    if err != nil {
        return "", err
    }
}

1.6.2【必须】密钥存储安全

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

// bad
crypto/des,crypto/md5,crypto/sha1,crypto/rc4等。

// good
crypto/rsa,crypto/aes等。

1.7 正则表达式

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

// good
matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err)     // nil

后台类

1 代码实现类

1.1 输入校验

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

// good
import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

var validate *validator.Validate

func validateVariable() {
    myEmail := "abc@tencent.com"
    errs := validate.Var(myEmail, "required,email")
    if errs != nil {
        fmt.Println(errs)
        return
        //停止执行
    }
    // 验证通过,继续执行
    ...
}

func main() {
    validate = validator.New()
    validateVariable()
}
import (
    "text/template"
)

// TestHTMLEscapeString HTML特殊字符转义
func main(inputValue string) string {
    escapedResult := template.HTMLEscapeString(inputValue)
    return escapedResult
}

1.2 SQL操作

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

import (
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

...
var product Product
...
db.First(&product, 1)
// bad
import (
    "database/sql"
    "fmt"
    "net/http"
)

func handler(db *sql.DB, req *http.Request) {
    q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE",
        req.URL.Query()["category"])
    db.Query(q)
}

// good
func handlerGood(db *sql.DB, req *http.Request) {
    // 使用?占位符
    q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE"
    db.Query(q, req.URL.Query()["category"])
}

1.3 网络请求

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

import (
    "encoding/xml"
    "fmt"
    "os"
)

func main() {
    type Person struct {
        XMLName  xml.Name `xml:"person"`
        Id       int      `xml:"id,attr"`
        UserName string   `xml:"name>first"`
        Comment  string   `xml:",comment"`
    }

    v := &Person{Id: 13, UserName: "John"}
    v.Comment = " Need more details. "

    enc := xml.NewEncoder(os.Stdout)
    enc.Indent("  ", "    ")
    if err := enc.Encode(v); err != nil {
        fmt.Printf("error: %v\n", err)
    }

}

1.4 服务器端渲染

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

// bad
func handler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    x := r.Form.Get("name")

    var tmpl = `<!DOCTYPE html><html><body>
    <form action="/" method="post">
        First name:<br>
    <input type="text" name="name" value="">
    <input type="submit" value="Submit">
    </form><p>` + x + ` </p></body></html>`

    t := template.New("main")
    t, _ = t.Parse(tmpl)
    t.Execute(w, "Hello")
}

// good
import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

var validate *validator.Validate
validate = validator.New()

func validateVariable(val) {
    errs := validate.Var(val, "gte=1,lte=100") // 限制必须是1-100的正整数
    if errs != nil {
        fmt.Println(errs)
        return false
    }
    return true
}

func handler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    x := r.Form.Get("name")

    if validateVariable(x) {
        var tmpl = `<!DOCTYPE html><html><body>
            <form action="/" method="post">
            First name:<br>
            <input type="text" name="name" value="">
            <input type="submit" value="Submit">
            </form><p>` + x + ` </p></body></html>`
        t := template.New("main")
        t, _ = t.Parse(tmpl)
        t.Execute(w, "Hello")
    } else {
        // ...
    }
}

1.5 Web跨域

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

// good
c := cors.New(cors.Options{
    AllowedOrigins:   []string{"http://qq.com", "https://qq.com"},
    AllowCredentials: true,
    Debug:            false,
})

// 引入中间件
handler = c.Handler(handler)

1.6 响应输出

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

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

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

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

import (
    "html/template"
)

func outtemplate(w http.ResponseWriter, r *http.Request) {
    param1 := r.URL.Query().Get("param1")
    tmpl := template.New("hello")
    tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
    tmpl.ExecuteTemplate(w, "T", param1)
}

1.7 会话管理

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

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

//...

cookie := http.Cookie{
    Name:     "Auth",
    Value:    signedToken,
    Expires:  expireCookie, // 过期失效
    HttpOnly: true,
    Path:     "/",
    Domain:   "127.0.0.1",
    Secure:   true,
}

http.SetCookie(res, &cookie)
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.7.2【必须】CSRF防护

- 涉及系统敏感操作或可读取敏感信息的接口应校验`Referer`或添加`csrf_token`。

```go
// good
import (
    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
    "net/http"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm)
    // 使用csrf_token验证
    http.ListenAndServe(":8000",
        csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}

1.8 访问控制

1.8.1【必须】默认鉴权

1.9 并发保护

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

// bad
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    var group sync.WaitGroup

    for i := 0; i < 5; i++ {
        group.Add(1)
        go func() {
            defer group.Done()
            fmt.Printf("%-2d", i) // 这里打印的i不是所期望的
        }()
    }
    group.Wait()
}

// good
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    var group sync.WaitGroup

    for i := 0; i < 5; i++ {
        group.Add(1)
        go func(j int) {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Println("Recovered in start()")
                }
                group.Done()
            }()
            fmt.Printf("%-2d", j) // 闭包内部使用局部变量
        }(i) // 把循环变量显式地传给协程
    }
    group.Wait()
}

1.9.2【必须】禁止并发写map

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

通过同步锁共享内存

// good
var count int

func Count(lock *sync.Mutex) {
    lock.Lock() // 加写锁
    count++
    fmt.Println(count)
    lock.Unlock() // 解写锁,任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()
}

func main() {
    lock := &sync.Mutex{}
    for i := 0; i < 10; i++ {
        go Count(lock) // 传递指针是为了防止函数内的锁和调用锁不一致
    }
    for {
        lock.Lock()
        c := count
        lock.Unlock()
        runtime.Gosched() // 交出时间片给协程
        if c > 10 {
            break
        }
    }
}
// good
import (
    "sync"
    "sync/atomic"
)

func main() {
    type Map map[string]string
    var m atomic.Value
    m.Store(make(Map))
    var mu sync.Mutex // used only by writers
    read := func(key string) (val string) {
        m1 := m.Load().(Map)
        return m1[key]
    }
    insert := func(key, val string) {
        mu.Lock() // 与潜在写入同步
        defer mu.Unlock()
        m1 := m.Load().(Map) // 导入struct当前数据
        m2 := make(Map)      // 创建新值
        for k, v := range m1 {
            m2[k] = v
        }
        m2[key] = val
        m.Store(m2) // 用新的替代当前对象
    }
    _, _ = read, insert
}

安全