扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 宝典简介

    • Go代码安全指南
  • 宝典内容

    • 后台类
    • 通用类

扫码订阅《 》或入驻星球,即可阅读文章!

通用类


腾讯

# 1.1 内存管理

# 1.1.1【必须】切片长度校验

  • 在对slice进行操作时,必须判断长度是否合法,防止程序panic
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 1.1.2【必须】nil指针判断

  • 进行指针操作时,必须判断该指针是否为nil,防止程序panic,尤其在进行结构体Unmarshal时
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)
        // Unmarshal(b[i:], p.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

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

  • 在进行make分配内存时,需要对外部可控的长度进行校验,防止程序panic。
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

  • 当一个对象从被GC选中到移除内存之前,runtime.SetFinalizer()都不会执行,即使程序正常结束或者发生错误。由指针构成的“循环引用”虽然能被GC正确处理,但由于无法确定Finalizer依赖顺序,从而无法调用runtime.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

  • 重复释放一般存在于异常流程判断中,如果恶意攻击者构造出异常条件使程序重复释放channel,则会触发运行时恐慌,从而造成DoS攻击。
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

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

# 1.1.8【推荐】不使用unsafe包

  • 由于unsafe包绕过了 Golang 的内存安全原则,一般来说使用该库是不安全的,可导致内存破坏,尽量避免使用该包。若必须要使用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
2
3
4
5
6
7
8

# 1.1.9【推荐】不使用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)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 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 {
		p, _ := filepath.Abs(f.Name)
		if !strings.Contains(p, "..") {
			ioutil.WriteFile(p, []byte("present"), 0640)
		}
	}
	return true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 1.2.2【必须】 文件访问权限

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

# 1.3 系统接口

# 1.3.1【必须】命令执行检查

  • 使用exec.Command、exec.CommandContext、syscall.StartProcess、os.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

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

	cmd := exec.Command("sh", "-c", cmdName)
	output, _ := cmd.CombinedOutput()
	fmt.Println(string(output))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 1.4 通信安全

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

  • 明文传输的通信协议目前已被验证存在较大安全风险,被中间人劫持后可能导致许多安全风险,因此必须采用至少TLS的安全通信方式保证通信安全,例如gRPC/Websocket都使用TLS1.3。
// 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
2
3
4
5
6
7
8
9
10

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

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 避免通过GET方法、代码注释、自动填充、缓存等方式泄露敏感信息

# 1.5.3【必须】敏感数据存储

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

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

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

错误例子:

dlv --listen=:2345 --headless=true --api-version=2 debug test.go
1

正确例子:

dlv debug test.go
1

# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 1.6.2【必须】密钥存储安全

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

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

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

错误例子:

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

# 1.7 正则表达式

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

  • 正则表达式编写不恰当可被用于DoS攻击,造成服务不可用,推荐使用regexp包进行正则表达式匹配。regexp保证了线性时间性能和优雅的失败:对解析器、编译器和执行引擎都进行了内存限制。但regexp不支持以下正则表达式特性,如业务依赖这些特性,则regexp不适合使用。
    • 回溯引用Backreferences (opens new window)和查看Lookaround (opens new window)
// good
matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err)     // nil (regexp is valid)
1
2
3
4
  • 1.1 内存管理
  • 1.1.1【必须】切片长度校验
  • 1.1.2【必须】nil指针判断
  • 1.1.4【必须】make分配长度验证
  • 1.1.5【必须】禁止SetFinalizer和指针循环引用同时使用
  • 1.1.6【必须】禁止重复释放channel
  • 1.1.7【必须】确保每个协程都能退出
  • 1.1.8【推荐】不使用unsafe包
  • 1.1.9【推荐】不使用slice作为函数入参
  • 1.2 文件操作
  • 1.2.1【必须】 路径穿越检查
  • 1.2.2【必须】 文件访问权限
  • 1.3 系统接口
  • 1.3.1【必须】命令执行检查
  • 1.4 通信安全
  • 1.4.1【必须】网络通信采用TLS方式
  • 1.4.2【推荐】TLS启用证书验证
  • 1.5 敏感数据保护
  • 1.5.1【必须】敏感信息访问
  • 1.5.2【必须】敏感数据输出
  • 1.5.3【必须】敏感数据存储
  • 1.5.4【必须】异常处理和日志记录
  • 1.6 加密解密
  • 1.6.1【必须】不得硬编码密码/密钥
  • 1.6.2【必须】密钥存储安全
  • 1.6.3【推荐】不使用弱密码算法
  • 1.7 正则表达式
  • 1.7.1【推荐】使用regexp进行正则表达式匹配