本代码安全指南由腾讯发布,项目地址:https://github.com/Tencent/secguide
// 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
}
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)
}
在进行数字运算操作时,需要做好长度限制,防止外部输入运算导致异常:
以下场景必须严格进行长度限制:
// 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)
}
// 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
}
// 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)
}
}
// 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
}
// bad: 协程没有设置退出条件
func doWaiter(name string, second int) {
for {
time.Sleep(time.Duration(second) * time.Second)
fmt.Println(name, " is ready!")
}
}
// 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]
// 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)
}
// 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
}
-rw-r-----
ioutil.WriteFile(p, []byte("present"), 0640)
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))
}
// 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))
}
// 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
}
// 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)
}
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
// 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等。
// good
matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err) // nil
validator
进行白名单校验,校验内容包括但不限于数据长度、数据范围、数据类型与格式,校验不通过的应当拒绝// 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()
}
html.EscapeString
、text/template
或bluemonday
对<, >, &, ',"
等字符进行过滤或编码import (
"text/template"
)
// TestHTMLEscapeString HTML特殊字符转义
func main(inputValue string) string {
escapedResult := template.HTMLEscapeString(inputValue)
return escapedResult
}
database/sql
的prepare、Query或使用GORM等ORM执行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"])
}
使用"net/http"
下的方法http.Get(url)
、http.Post(url, contentType, body)
、http.Head(url)
、http.PostForm(url, data)
、http.Do(req)
时,如变量值外部可控(指从参数中动态获取),应对请求目标进行严格的安全校验。
如请求资源域名归属固定的范围,如只允许a.qq.com
和b.qq.com
,应做白名单限制。如不适用白名单,则推荐的校验逻辑步骤是:
第 1 步、只允许HTTP或HTTPS协议
第 2 步、解析目标URL,获取其HOST
第 3 步、解析HOST,获取HOST指向的IP地址转换成Long型
第 4 步、检查IP地址是否为内网IP,网段有:
// 以RFC定义的专有网络为例,如有自定义私有网段亦应加入禁止访问列表。
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
127.0.0.0/8
第 5 步、请求URL
第 6 步、如有跳转,跳转后执行1,否则绑定经校验的ip和域名,对URL发起请求
官方库encoding/xml
不支持外部实体引用,使用该库可避免xxe漏洞
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)
}
}
text/template
或者html/template
渲染模板时禁止将外部输入参数引入模板,或仅允许引入白名单内字符。// 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 {
// ...
}
}
// good
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://qq.com", "https://qq.com"},
AllowCredentials: true,
Debug: false,
})
// 引入中间件
handler = c.Handler(handler)
application/json
;若为xml,则设置为text/xml
。X-Content-Type-Options: nosniff
。X-Frame-Options
。按需合理设置其允许范围,包括:DENY
、SAMEORIGIN
、ALLOW-FROM origin
。用法参考:MDN文档\r
、\n
等换行符,或者拒绝携带换行符号的外部输入。text/template
自动编码,或者使用html.EscapeString
或text/template
对<, >, &, ',"
等字符进行编码。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)
}
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)
//...
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))
}
除非资源完全可对外开放,否则系统默认进行身份认证,使用白名单的方式放开不需要认证的接口或页面。
根据资源的机密程度和用户角色,以最小权限原则,设置不同级别的权限,如完全公开、登录可读、登录可写、特定用户可读、特定用户可写等
涉及用户自身相关的数据的读写必须验证登录态用户身份及其权限,避免越权操作
-- 伪代码
select id from table where id=:id and userid=session.userid
没有独立账号体系的外网服务使用QQ
或微信
登录,内网服务使用统一登录服务
登录,其他使用账号密码登录的服务需要增加验证码等二次验证
// 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()
}
// 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.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
}
}
}
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) // 导入struct当前数据
m2 := make(Map) // 创建新值
for k, v := range m1 {
m2[k] = v
}
m2[key] = val
m.Store(m2) // 用新的替代当前对象
}
_, _ = read, insert
}