# 1.内存管理

切片长度校验

操作 slice 时,必须判断长度是否合法,防止程序 panic。

// bad: slice bounds out of range
func foo(slice []int){
    fmt.Println(slice[:10])
}

// good: check the slice length
func foo(slice []int){
    if len(slice) >= 10 {
        fmt.Println(slice[:10])
        return
    }
    fmt.Println("no enough elems in slice")
}

指针判空

进行指针操作时,必须判断该指针是否为 nil,防止程序 panic,尤其在 Unmarshal 结构体时。

type Packet struct {
    Type    uint8
    Version uint8
    Data    *Data
}
type Data struct {
    Stat uint8
    Len  uint8
    Buf  [8]byte
}

// bad
func foo(p Packet) {
    fmt.Println(p.Data.Stat)
}

// good
func foo(p Packet) {
    if p.Data != nil {
        fmt.Println(p.Data.Stat)
        return
    }
    fmt.Println("packet is nil")
}

整数安全

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

  • 确保无符号整数运算时不会出现回绕

无符号整型(如 uint8、uint16、uint32、uint64 和 uint)在达到其类型能够表示的最大值时,如果继续增加,就会从零开始重新计数,这种现象称为“回绕”(Wraparound)。

  • 确保有符号整数运算时不会出现溢出
// bad:未限制长度,可能导致整数溢出
func overflow(n int32) {
    // 当 n 为 math.MaxInt32 (2147483647) 时溢出
    // num 为 math.MaxInt32 (-2147483648)
    num := n + 1
    fmt.Printf("%d\n", num)
    // 使用 num,可能导致其他错误
}

// good: 对结果进行溢出判断
func overflow(n int32) {
    num := n + 1
    if num < 0 {
        fmt.Println("integer overflow")
        return
    }
    fmt.Println("integer ok")
}
  • 确保整型转换时不会出现截断错误
  • 确保整型转换时不会出现符号错误

以下场景必须严格进行长度限制:

  • 作为数组索引
  • 作为对象的长度或者大小
  • 作为数组的边界(如作为循环计数器)

    make 分配长度验证

    使用 make 分配内存时,需要对外部输入长度进行校验,防止程序 panic。 ```go // 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 > 6410241024 { return nil, errors.New("value too large") } buffer := make([]byte, size) copy(buffer, data) return buffer, nil }

## 禁止 SetFinalizer 和指针引用同时使用
> runtime.SetFinalizer 是一个用于设置终结器(finalizer)的方法。终结器是一个函数,它会在对象被垃圾回收(GC)前执行。通过使用 runtime.SetFinalizer,你可以在对象的生命周期结束时执行一些清理操作,比如释放外部资源、关闭文件或网络连接等。

当一个对象从被GC选中到移除内存之前,runtime.SetFinalizer() 都不会执行,即使程序正常结束或者发生错误。由指针构成的"循环引用"虽然能被Go正确处理,但由于无法确定 Finalizer 依赖顺序,从而无法调用 runtime.SetFinalizer() 导致目标对象无法变成可达状态,从而造成内存无法被回收。
```go
// 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)
    }
}

禁止重复释放 channel

重复释放一般存在于异常流程判断中,如果恶意攻击者构造出异常条件使程序重复释放 channel,则会触发运行时 panic,从而造成 DoS 攻击。

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

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

确保每个协程都能退出

启动一个协程就会做一个入栈操作,在系统不退出的情况下协程也没有设置退出条件,则相当于协程失去了控制,它占用的资源无法回收,可能会导致内存泄露。

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

尽量不使用 unsafe 包

由于 unsafe 包绕过了 Golang 的内存安全原则,一般来说使用该库是不安全的,可导致内存破坏,尽量避免使用该包。

若必须使用 unsafe 操作指针,必须做好安全校验。

// bad: 通过 unsafe 操作原始指针
func unsafePointer() {
    b := make([]byte, 1)
    foo := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(0xffffffffe)))
    fmt.Print(*foo + 1)
}}
// [signal SIGSEGV: segmentation violation code=0x1 addr=0xc100068f55 pc=0x49142b]

使用 slice 作为函数入参时当心数据被修改

slice 在作为函数入参时,函数内对 slice 的修改可能会影响原数据。如果希望不被修改,请使用数组替代 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)
}

2.文件操作

路径穿越检查

在进行文件操作时,如果对外部传入的文件名未做限制,可能导致任意文件读取或者任意文件写入,严重可能导致意外代码被执行。

//bad:任意文件读取
func handler(w http.ResponseWriter, r *http.Requeest){
    path := r.URL.Query()["path"][0]
    //未过滤文件路径,可能导致任意文件读取
    data,_ := ioutil.ReadFile(path)
    w.Write(data)
    //对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
    data, _ = ioutil.ReadFile(filepath.Join("/home/usser/",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, "..") {
            return false
        }
        p, _ := filepath.Abs(f.Name)
        ioutil.WriteFile(p, []byte("present"), 0640)
    }
    return true
}

文件访问权限

根据创建文件的敏感性设置不同级别的访问权限,以防止敏感数据被任意权限用户读取。

例如,设置文件权限为:

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

3.系统接口

命令执行检查

在使用处理外部进程的函数时,诸如

  • exec.Command
  • exec.CommandContext
  • os.StartProcess
  • syscall.StartProcess

第一个参数(path)直接取外部输入值时,应使用白名单限制可执行命令的范围,不允许传入 bash、cmd、sh 等命令。

使用 exec.Command、exec.CommandContext 等函数时,通过 bash、cmd、sh 等创建 shell,-c 后的参数(arg)拼接外部输入,应过滤\n $ & ; ' " ()等潜在恶意字符。

// 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
    // 检查传给sh的命令是否有特殊字符
    if checkIllegal(cmdName) {
        return // 存在特殊字符直接return
    }
    cmd := exec.Command("sh", "-c", cmdName)
    output, _ := cmd.CombinedOutput()
    fmt.Println(string(output))
}

4.敏感数据保护

敏感信息访问

禁止将敏感信息硬编码在程序中,既可能会将敏感信息暴露给攻击者,也会增加代码管理和维护的难度。

敏感数据输出

  • 只输出必要的最小数据集,避免多余字段暴露引起敏感信息泄露。
  • 不能在日志保存密码(包括明文密码和密文密码)、密钥和其它敏感信息。
  • 对于必须输出的敏感信息,必须进行合理脱敏展示
// bad
func serve(){
    http.HandleFunc("/register", func(w http.ResponseeWriter, r *http.Request){
        r.ParseForm
        user := r.Form.Get("user")
        pw := r.Form.Get("password")
        log.Printf("Registering new user %s with passwordd %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)
}
  • 避免通过GET方法、代码注释、自动填充、缓存等方式泄露敏感信息

敏感数据存储

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

异常处理和日志记录

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

// good
dlv debug test.go

5.加密解密

不得硬编码密码/密钥

在进行用户登录、加解密等操作时,不得在代码里硬编码密钥或密码,可通过变换算法或者配置等方式设置密码或者密钥。

// 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
    }
}

密钥存储安全

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

不使用弱密码算法

在使用加密算法时,不建议使用加密强度较弱的算法。

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

// good
crypto/rsa, crypto/aes 等

6.并发保护

禁止在闭包中直接调用循环变量

在循环中启动协程,当协程中使用到了循环的索引值,由于多个协程同时使用同一个变量会产生数据竞争,造 成执行结果异常。

// 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 err := recover(); r != nil {
                    fmt.Println("Recovered in start()")
                }
                group.Done()
            }()
            fmt.Printf("%-2d",j)//闭包内部使用局部变量
        }(i)//把循环变量显式地传给协程
    }
    group.Wait()
}

禁止并发写map

并发写 map 容易造成程序崩溃并异常退出,推荐使用 sync.Map。

// bad
func main() {
    m := make(map[int]int)
    // 并发读写
    go func(){
        for {
            _ = m[1]
        }
    }()
    go func() {
        for {
            m[2] = 1
        }
    }()
    select {}
}

确保并发安全

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

  • 通过同步锁共享内存
// good
var count int
func Count(lock *sync.Mutex) {
    // 加写锁
    lock.Lock()
    count++
    fmt.Println(count)
    // 解写锁,任何一个Lock()或RLock()均需要保证对应有UnLock()或RUnLock ()
    lock.Unlock()
}

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
    }
}
  • 使用 sync/atomic 执行原子操作
// 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)
        m2 := make(Map)
        // 导入struct当前数据
        // 创建新值
        for k, v := range m1 {
            m2[k] = v
        }
        m2[key] = val
        m.Store(m2)//用新的替代当前对象
    }
    _, _ = read, insert
}
powered by Gitbook该文章修订时间: 2024-08-04 07:27:05

results matching ""

    No results matching ""