昇腾社区 Go 语言安全编程指导(建议稿)

说明

本指导基于Go语言制定而成,给参与Ascend开源社区项目的开发者提供安全编程指导。

规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的是"为了大多数程序员可以得到更多的好处"。

参考本指导之前,希望您具有相应的Go语言基础能力,而不是通过该文档来学习Go语言。

  1. 了解Go语言的语法和特性;
  2. 熟知Go语言的基本语言特性,包括Go 1.x相关特性;
  3. 了解Go语言的标准库;

如果希望改进某个规则,建议提交Issue并说明理由,经Ascend运营团队评审后可接纳并修改生效。

约定

规则:编程时必须遵守的约定(must)

建议:编程时应该遵守的约定(should)

本指导适用Go 1.x版本,如果没有特定的版本要求,适用所有Go 1.x版本。

例外

无论是'规则'还是'建议',都必须理解该条目这么规定的原因,并努力遵守。 但是,有些规则和建议可能会有例外。

在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背本指导中约定。 例外破坏了代码的一致性,请尽量避免。'规则'的例外应该是极少的。

适用范围

Ascend 社区所有开源仓


1. 安全编码

1.1 总体规则

规则 1.1.1 保证类型安全

Go语言是静态类型语言,应该充分利用类型系统来保证类型安全。避免不必要的类型转换和类型断言。

// Good
func ProcessInt(value int) {
    // ...
}

// Bad
func ProcessInt(value interface{}) {
    intValue := value.(int)  // 不安全的类型断言
    // ...
}
规则 1.1.2 避免使用unsafe包

除非有充分的理由,否则应该避免使用unsafe包。使用unsafe包会破坏类型安全,可能导致内存安全问题。

// Good
func CopySlice(dst, src []byte) {
    copy(dst, src)
}

// Bad
import "unsafe"

func CopySlice(dst, src []byte) {
    // 使用unsafe可能导致内存安全问题
    unsafe.Copy(unsafe.Pointer(&dst[0]), unsafe.Pointer(&src[0]), len(src))
}
规则 1.1.3 禁止使用未定义行为

遵循Go语言规范,禁止使用规范中未定义的行为。对于编译器实现的特性或者扩展特性也需要谨慎使用,这些特性会降低代码的可移植性。

1.2 输入验证

规则 1.2.1 对所有外部输入进行验证和清理

所有来自外部的输入(用户输入、网络数据、文件内容、环境变量等)都必须进行验证和清理。

// Good
func ProcessUserInput(input string) error {
    // 验证输入长度
    if len(input) > MaxInputLength {
        return errors.New("input too long")
    }
    
    // 验证输入格式
    if !isValidFormat(input) {
        return errors.New("invalid input format")
    }
    
    // 清理输入(去除危险字符等)
    cleaned := sanitizeInput(input)
    
    // 处理清理后的输入
    return process(cleaned)
}

// Bad
func ProcessUserInput(input string) error {
    // 直接使用未验证的输入
    return process(input)
}
规则 1.2.2 外部数据作为数组索引或切片操作时必须校验边界
// Good
func GetElement(slice []int, index int) (int, error) {
    if index < 0 || index >= len(slice) {
        return 0, errors.New("index out of range")
    }
    return slice[index], nil
}

// Bad
func GetElement(slice []int, index int) int {
    return slice[index]  // 可能导致panic
}
规则 1.2.3 禁止直接使用外部数据拼接SQL命令

必须使用参数化查询或预编译语句,防止SQL注入攻击。

// Good
func GetUser(db *sql.DB, id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).Scan(
        &user.ID, &user.Name, &user.Email)
    return &user, err
}

// Bad
func GetUser(db *sql.DB, id string) (*User, error) {
    query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)  // SQL注入风险
    // ...
}
规则 1.2.4 禁止直接使用外部数据构造命令执行

禁止直接使用外部数据构造系统命令,防止命令注入攻击。

// Good
func ExecuteCommand(cmd string, args []string) error {
    execCmd := exec.Command(cmd, args...)
    return execCmd.Run()
}

// Bad
func ExecuteCommand(userInput string) error {
    cmd := exec.Command("sh", "-c", userInput)  // 命令注入风险
    return cmd.Run()
}

1.3 表达式与语句

规则 1.3.1 确保整数运算不溢出

对于可能来自外部数据的整数运算,需要确保不会导致溢出。

// Good
func SafeAdd(a, b int) (int, error) {
    if a > 0 && b > math.MaxInt-a {
        return 0, errors.New("integer overflow")
    }
    if a < 0 && b < math.MinInt-a {
        return 0, errors.New("integer underflow")
    }
    return a + b, nil
}

// Bad
func UnsafeAdd(a, b int) int {
    return a + b  // 可能溢出
}
规则 1.3.2 确保除法和取余运算不会导致除零错误
// Good
func SafeDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Bad
func UnsafeDivide(a, b int) int {
    return a / b  // 除零会导致panic
}
规则 1.3.3 &&和||操作符的右侧操作数不要包含副作用

逻辑与(&&)、逻辑或(||)表达式中的右操作数是否被求值,取决于左操作数的求值结果。如果右操作数包含副作用,则不能确定是否确实发生了副作用。

// Good
if isValid && processData() {
    // processData只在isValid为true时调用
}

// Bad
if isValid && (count++ > 0) {
    // count++的副作用不确定是否发生
}
规则 1.3.4 循环必须安全退出

在应用程序中,一个重复提供服务的逻辑循环应当设计退出机制,并且将资源正确释放后安全退出。

// Good
func ProcessLoop(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        case data := <-dataChan:
            process(data)
        }
    }
}

// Bad
func ProcessLoop() {
    for {
        process(data)  // 无法退出
    }
}

1.4 资源管理

规则 1.4.1 禁止将局部变量的地址传递到其作用域外
// Good
func GetValue() int {
    value := 42
    return value
}

// Bad
func GetPointer() *int {
    value := 42
    return &value  // 返回局部变量的地址,危险
}
规则 1.4.2 禁止解引用空指针

解引用空指针会导致运行时panic,在使用指针前必须检查是否为nil。

// Good
func ProcessUser(user *User) error {
    if user == nil {
        return errors.New("user is nil")
    }
    return user.Process()
}

// Bad
func ProcessUser(user *User) error {
    return user.Process()  // 如果user为nil,会导致panic
}
规则 1.4.3 禁止对nil接口调用方法

对nil接口调用方法会导致运行时panic,在使用接口前必须检查是否为nil。

// Good
func ProcessWriter(w io.Writer) error {
    if w == nil {
        return errors.New("writer is nil")
    }
    _, err := w.Write([]byte("data"))
    return err
}

// Bad
func ProcessWriter(w io.Writer) error {
    _, err := w.Write([]byte("data"))  // 如果w为nil,会导致panic
    return err
}
规则 1.4.4 切片和map操作前必须检查nil

虽然对nil切片进行某些操作(如range)不会panic,但可能导致逻辑错误。对nil map进行操作会导致panic。

// Good
func ProcessSlice(s []int) {
    if s == nil {
        return
    }
    // 处理切片
}

func ProcessMap(m map[string]int) {
    if m == nil {
        return
    }
    // 处理map
}

// Bad
func ProcessSlice(s []int) {
    for i := range s {  // 如果s为nil,虽然不会panic,但逻辑可能不正确
        // ...
    }
}

func ProcessMap(m map[string]int) {
    value := m["key"]  // 如果m为nil,会导致panic
}
规则 1.4.5 禁止对nil channel进行操作

对nil channel进行发送、接收或关闭操作会导致运行时panic或永久阻塞。

// Good
func SendData(ch chan<- int, data int) error {
    if ch == nil {
        return errors.New("channel is nil")
    }
    ch <- data
    return nil
}

func CloseChannel(ch chan int) error {
    if ch == nil {
        return errors.New("channel is nil")
    }
    close(ch)
    return nil
}

// Bad
func SendData(ch chan<- int, data int) {
    ch <- data  // 如果ch为nil,会导致永久阻塞
}

func CloseChannel(ch chan int) {
    close(ch)  // 如果ch为nil,会导致panic
}
规则 1.4.6 禁止对nil函数类型进行调用

对nil函数类型进行调用会导致运行时panic。

// Good
type Handler func() error

func ExecuteHandler(h Handler) error {
    if h == nil {
        return errors.New("handler is nil")
    }
    return h()
}

// Bad
type Handler func() error

func ExecuteHandler(h Handler) error {
    return h()  // 如果h为nil,会导致panic
}

1.5 错误处理

规则 1.5.1 错误信息不应泄露敏感信息

错误信息应该提供有用的上下文,但不应该泄露敏感信息(如密码、密钥、内部路径等)。

// Good
if err != nil {
    return fmt.Errorf("authentication failed")
}

// Bad
if err != nil {
    return fmt.Errorf("authentication failed: password %s is incorrect", password)
}
规则 1.5.2 使用errors.Is和errors.As进行错误检查

使用errors.Iserrors.As来检查和处理特定类型的错误,而不是直接比较错误值。

// Good
if errors.Is(err, os.ErrNotExist) {
    // 处理文件不存在的情况
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    // 处理路径错误
}

// Bad
if err == os.ErrNotExist {  // 可能无法匹配包装的错误
    // ...
}

1.6 标准库

规则 1.6.1 调用格式化函数时,禁止format参数受外部数据控制
// Good
log.Printf("User %s logged in", sanitize(username))

// Bad
format := userInput  // 来自外部输入
log.Printf(format, username)  // 格式化字符串注入风险
规则 1.6.2 使用strings包进行字符串操作,避免手动操作字节
// Good
if strings.Contains(str, substr) {
    // ...
}

// Bad
if bytes.Contains([]byte(str), []byte(substr)) {  // 对于字符串,应该使用strings包
    // ...
}
规则 1.6.3 禁止使用os.Exit和log.Fatal(除了main函数)

使用os.Exitlog.Fatal会立即终止程序,导致defer函数无法执行,资源无法正确释放。

// Good
func Process() error {
    if err := doSomething(); err != nil {
        return err  // 返回错误,让调用者处理
    }
    return nil
}

// Bad
func Process() {
    if err := doSomething(); err != nil {
        log.Fatal(err)  // 立即退出,defer不会执行
    }
}

1.7 并发安全

规则 1.7.1 共享资源访问必须使用同步机制

多个goroutine访问共享资源时,必须使用mutex、channel等同步机制,避免数据竞争。

// Good
type SafeCounter struct {
    mu    sync.RWMutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Get() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.count
}

// Bad
type UnsafeCounter struct {
    count int
}

func (c *UnsafeCounter) Increment() {
    c.count++  // 并发不安全,存在数据竞争
}
规则 1.7.2 尽量缩短在临界区内停留的时间

在持有锁的情况下,应尽量减少执行时间,以提高程序的并发性能。长时间持有锁可能导致其他goroutine长时间等待,降低系统吞吐量。

// Good
func (c *Counter) Process(data []byte) error {
    // 在临界区外进行耗时操作
    processed := expensiveOperation(data)
    
    // 只在必要时进入临界区
    c.mu.Lock()
    c.count += len(processed)
    c.mu.Unlock()
    
    return nil
}

// Bad
func (c *Counter) Process(data []byte) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // 在临界区内进行耗时操作,会阻塞其他goroutine
    processed := expensiveOperation(data)
    c.count += len(processed)
    
    return nil
}
规则 1.7.3 避免goroutine被永久阻塞

确保goroutine能够正常退出,避免因channel未关闭、死锁等原因导致goroutine永久阻塞。

// Good
func ProcessWithTimeout(ctx context.Context, ch <-chan int) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case data, ok := <-ch:
            if !ok {
                return nil  // channel已关闭,正常退出
            }
            process(data)
        }
    }
}

// Bad
func Process(ch <-chan int) {
    for {
        data := <-ch  // 如果channel未关闭且没有发送者,会永久阻塞
        process(data)
    }
}
规则 1.7.4 使用带超时的channel操作

对channel操作设置超时,避免goroutine永久阻塞。

// Good
func SendWithTimeout(ch chan<- int, data int, timeout time.Duration) error {
    select {
    case ch <- data:
        return nil
    case <-time.After(timeout):
        return errors.New("send timeout")
    }
}

func ReceiveWithTimeout(ch <-chan int, timeout time.Duration) (int, error) {
    select {
    case data := <-ch:
        return data, nil
    case <-time.After(timeout):
        return 0, errors.New("receive timeout")
    }
}

// Bad
func Send(ch chan<- int, data int) {
    ch <- data  // 如果channel已满且没有接收者,会永久阻塞
}
规则 1.7.5 避免死锁

确保锁的获取顺序一致,避免多个goroutine相互等待导致死锁。

// Good
func Transfer(from, to *Account, amount int) error {
    // 使用一致的锁获取顺序(按地址排序)
    first, second := from, to
    if uintptr(unsafe.Pointer(from)) > uintptr(unsafe.Pointer(to)) {
        first, second = to, from
    }
    
    first.mu.Lock()
    defer first.mu.Unlock()
    second.mu.Lock()
    defer second.mu.Unlock()
    
    // 执行转账操作
    return nil
}

// Bad
func Transfer(from, to *Account, amount int) error {
    from.mu.Lock()
    to.mu.Lock()  // 如果另一个goroutine同时执行反向转账,可能导致死锁
    defer from.mu.Unlock()
    defer to.mu.Unlock()
    
    // 执行转账操作
    return nil
}
规则 1.7.6 使用context控制goroutine生命周期

使用context来控制和取消goroutine,避免goroutine泄漏。

// Good
func Worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            return  // 收到取消信号,退出goroutine
        case job, ok := <-jobs:
            if !ok {
                return  // channel已关闭,退出goroutine
            }
            processJob(job)
        }
    }
}

// Bad
func Worker(jobs <-chan Job) {
    for job := range jobs {
        processJob(job)  // 如果context被取消,无法退出
    }
}

1.8 内存

规则 1.8.1 严禁使用string类型存储敏感信息

string类型在Go中是不可变的,敏感信息(如密码、密钥)存储在string中后,无法安全地清除。应该使用[]byte并在使用后清零。

// Good
func VerifyPassword(password []byte) bool {
    // 验证密码
    // 使用后清零
    for i := range password {
        password[i] = 0
    }
    return true
}

// Bad
func VerifyPassword(password string) bool {
    // string是不可变的,无法清零
    // 敏感信息可能残留在内存中
    return true
}
规则 1.8.2 内存中的敏感信息使用完毕后立即清零

口令、密钥等敏感信息使用完毕后立即清零,避免被攻击者获取。

// Good
func ProcessSensitiveData(data []byte) {
    // 处理敏感数据
    process(data)
    
    // 使用后清零
    for i := range data {
        data[i] = 0
    }
}

// Bad
func ProcessSensitiveData(data []byte) {
    // 处理敏感数据
    process(data)
    // 没有清零,敏感信息残留在内存中
}
规则 1.8.3 避免在日志中输出敏感信息

不要在日志、错误消息中输出敏感信息(密码、密钥、令牌等)。

// Good
log.Printf("User authentication failed for user ID: %d", userID)

// Bad
log.Printf("User authentication failed: password %s is incorrect", password)

1.9 文件

规则 1.9.1 外部文件路径使用前必须进行规范化并校验

当文件路径来自外部数据时,需要先将文件路径规范化,如果没有作规范化处理,攻击者就有机会通过恶意构造文件路径进行文件的越权访问。

// Good
func ReadFile(userInput string) ([]byte, error) {
    // 规范化路径
    cleanPath := filepath.Clean(userInput)
    absPath, err := filepath.Abs(cleanPath)
    if err != nil {
        return nil, err
    }
    
    // 验证路径是否在允许的目录下
    baseDir := "/allowed/directory"
    if !strings.HasPrefix(absPath, baseDir) {
        return nil, errors.New("invalid file path")
    }
    
    return ioutil.ReadFile(absPath)
}

// Bad
func ReadFile(userInput string) ([]byte, error) {
    return ioutil.ReadFile(userInput)  // 路径遍历风险
}
规则 1.9.2 不要在共享目录中创建临时文件

程序的临时文件应当是程序自身独享的,任何将自身临时文件置于共享目录的做法,将导致其他共享用户获得该程序的额外信息,产生信息泄露。

// Good
tmpFile, err := os.CreateTemp("", "prefix-*.tmp")
if err != nil {
    return err
}
defer os.Remove(tmpFile.Name())

// Bad
tmpFile, err := os.Create("/tmp/shared-file.tmp")  // 共享目录
规则 1.9.3 文件操作必须检查权限

创建或修改文件时,必须设置适当的文件权限,避免文件被未授权访问。

// Good
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0600)  // 仅所有者可读写
if err != nil {
    return err
}
defer f.Close()

// Bad
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666)  // 所有用户可读写

1.10 网络

规则 1.10.1 使用HTTPS进行网络通信

涉及敏感信息的网络通信必须使用HTTPS,禁止使用HTTP。

// Good
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
        },
    },
}

// Bad
resp, err := http.Get("http://example.com/api")  // 未加密
规则 1.10.2 验证TLS证书

使用HTTPS时,必须验证服务器证书,禁止跳过证书验证。

// Good
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
        },
    },
}

// Bad
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,  // 跳过证书验证,危险
        },
    },
}
规则 1.10.3 设置合理的超时时间

网络请求必须设置超时时间,避免无限等待。

// Good
client := &http.Client{
    Timeout: 30 * time.Second,
}

// Bad
client := &http.Client{}  // 没有超时设置

1.11 加密与随机数

规则 1.11.1 禁用math/rand产生用于安全用途的随机数

math/rand生成的是伪随机数,不适合用于安全用途。应该使用crypto/rand

// Good
import "crypto/rand"

func GenerateToken() (string, error) {
    tokenBytes := make([]byte, 32)
    _, err := rand.Read(tokenBytes)
    if err != nil {
        return "", err
    }
    return hex.EncodeToString(tokenBytes), nil
}

// Bad
import "math/rand"

func GenerateToken() string {
    return fmt.Sprintf("%d", rand.Int())  // 伪随机数,不安全
}
规则 1.11.2 使用标准库的加密函数

使用crypto包中的标准加密函数,不要自己实现加密算法。

// Good
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
)

func Encrypt(data []byte, key []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    // 使用标准库的加密函数
    // ...
}

// Bad
func Encrypt(data []byte, key []byte) []byte {
    // 自己实现的加密算法,不安全
    // ...
}
规则 1.11.3 使用强密码哈希算法

存储密码时,必须使用强密码哈希算法(如bcrypt、argon2),禁止使用MD5、SHA1等弱哈希算法。

// Good
import "golang.org/x/crypto/bcrypt"

func HashPassword(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(hash), nil
}

// Bad
import "crypto/sha256"

func HashPassword(password string) string {
    hash := sha256.Sum256([]byte(password))
    return hex.EncodeToString(hash[:])  // SHA256不适合用于密码哈希
}