Go安全指南

通用類

1. 代碼實現類

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.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)
    }
    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【必須】整數安全

  • 在進行數字運算操作時,需要做好長度限制求冷,防止外部輸入運算導致異常:

    • 確保無符號整數運算時不會反轉
    • 確保有符號整數運算時不會出現溢出
    • 確保整型轉換時不會出現截斷錯誤
    • 確保整型轉換時不會出現符號錯誤
  • 以下場景必須嚴格進行長度限制:

    • 作為數組索引
    • 作為對象的長度或者大小
    • 作為數組的邊界(如作為循環(huán)計數器)
// 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分配長度驗證

  • 在進行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.1.5【必須】禁止SetFinalizer和指針循環(huán)引用同時使用

  • 當一個對象從被GC選中到移除內存之前冷溃,runtime.SetFinalizer()都不會執(zhí)行钱磅,即使程序正常結束或者發(fā)生錯誤。由指針構成的“循環(huán)引用”雖然能被GC正確處理秃诵,但由于無法確定Finalizer依賴順序续搀,從而無法調用runtime.SetFinalizer()塞琼,導致目標對象無法變成可達狀態(tài)菠净,從而造成內存無法被回收。
// bad
func foo() {
    var a, b Data
    a.o = &b
    b.o = &a

    // 指針循環(huán)引用彪杉,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

  • 重復釋放一般存在于異常流程判斷中毅往,如果惡意攻擊者構造出異常條件使程序重復釋放channel,則會觸發(fā)運行時panic派近,從而造成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.1.7【必須】確保每個協(xié)程都能退出

  • 啟動一個協(xié)程就會做一個入棧操作,在系統(tǒng)不退出的情況下渴丸,協(xié)程也沒有設置退出條件侯嘀,則相當于協(xié)程失去了控制,它占用的資源無法回收谱轨,可能會導致內存泄露戒幔。
// bad: 協(xié)程沒有設置退出條件
func doWaiter(name string, second int) {
    for {
        time.Sleep(time.Duration(second) * time.Second)
        fmt.Println(name, " is ready!")
    }
}

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.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的區(qū)別
      array := [5]int{1, 2, 3, 4, 5}
  
      modify(array)
      fmt.Println(array)
  }
  

1.2 文件操作

1.2.1【必須】 路徑穿越檢查

  • 在進行文件操作時昭齐,如果對外部傳入的文件名未做限制,可能導致任意文件讀取或者任意文件寫入矾柜,嚴重可能導致代碼執(zhí)行司浪。
// 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【必須】 文件訪問權限

  • 根據創(chuàng)建文件的敏感性設置不同級別的訪問權限租谈,以防止敏感數據被任意權限用戶讀取。例如捆愁,設置文件權限為:-rw-r-----
ioutil.WriteFile(p, []byte("present"), 0640)

1.3 系統(tǒng)接口

1.3.1【必須】命令執(zhí)行檢查

  • 使用exec.Command割去、exec.CommandContextsyscall.StartProcess昼丑、os.StartProcess等函數時呻逆,第一個參數(path)直接取外部輸入值時,應使用白名單限定可執(zhí)行的命令范圍菩帝,不允許傳入bash咖城、cmdsh等命令呼奢;
  • 使用exec.Command宜雀、exec.CommandContext等函數時,通過bash握础、cmd辐董、sh等創(chuàng)建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.4 通信安全

1.4.1【必須】網絡通信采用TLS方式

  • 明文傳輸的通信協(xié)議目前已被驗證存在較大安全風險定枷,被中間人劫持后可能導致許多安全風險孤澎,因此必須采用至少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.4.2【推薦】TLS啟用證書驗證

  • TLS證書應當是有效的亥至、未過期的,且配置正確的域名,生產環(huán)境的服務端應啟用證書驗證姐扮。
// 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【必須】敏感信息訪問

  • 禁止將敏感信息硬編碼在程序中絮供,既可能會將敏感信息暴露給攻擊者,也會增加代碼管理和維護的難度
  • 使用配置中心系統(tǒng)統(tǒng)一托管密鑰等敏感信息

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)
}
  • 避免通過GET方法惊搏、代碼注釋贮乳、自動填充、緩存等方式泄露敏感信息

1.5.3【必須】敏感數據存儲

  • 敏感數據應使用SHA2恬惯、RSA等算法進行加密存儲
  • 敏感數據應使用獨立的存儲層向拆,并在訪問層開啟訪問控制
  • 包含敏感信息的臨時文件或緩存一旦不再需要應立刻刪除

1.5.4【必須】異常處理和日志記錄

  • 應合理使用panic、recover酪耳、defer處理系統(tǒng)異常浓恳,避免出錯信息輸出到前端
defer func () {
    if r := recover(); r != nil {
        fmt.Println("Recovered in start()")
    }
}()
  • 對外環(huán)境禁止開啟debug模式,或將程序運行日志輸出到前端
// 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【必須】密鑰存儲安全

  • 在使用對稱密碼算法時晴圾,需要保護好加密密鑰。當算法涉及敏感噪奄、業(yè)務數據時死姚,可通過非對稱算法協(xié)商加密密鑰。其他較為不敏感的數據加密梗醇,可以通過變換算法等方式保護密鑰知允。

1.6.3【推薦】不使用弱密碼算法

  • 在使用加密算法時,不建議使用加密強度較弱的算法叙谨。
// bad
crypto/des,crypto/md5保屯,crypto/sha1手负,crypto/rc4等。

// good
crypto/rsa姑尺,crypto/aes等竟终。

1.7 正則表達式

1.7.1【推薦】使用regexp進行正則表達式匹配

  • 正則表達式編寫不恰當可被用于DoS攻擊,造成服務不可用切蟋,推薦使用regexp包進行正則表達式匹配统捶。regexp保證了線性時間性能和優(yōu)雅的失敗:對解析器、編譯器和執(zhí)行引擎都進行了內存限制喘鸟。但regexp不支持以下正則表達式特性匆绣,如業(yè)務依賴這些特性,則regexp不適合使用什黑。
// good
matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err)     // nil

后臺類

1 代碼實現類

1.1 輸入校驗

1.1.1【必須】按類型進行數據校驗

  • 所有外部輸入的參數崎淳,應使用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
        //停止執(zhí)行
    }
    // 驗證通過恨豁,繼續(xù)執(zhí)行
    ...
}

func main() {
    validate = validator.New()
    validateVariable()
}
  • 無法通過白名單校驗的應使用html.EscapeString嚣镜、text/templatebluemonday<, >, &, ',"等字符進行過濾或編碼
import (
    "text/template"
)

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

1.2 SQL操作

1.2.1【必須】SQL語句默認使用預編譯并綁定變量

  • 使用database/sql的prepare、Query或使用GORM等ORM執(zhí)行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)
  • 使用參數化查詢橘蜜,禁止拼接SQL語句祈惶,另外對于傳入參數用于order by或表名的需要通過校驗
// 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【必須】資源請求過濾驗證

  • 使用"net/http"下的方法http.Get(url)http.Post(url, contentType, body)扮匠、http.Head(url)捧请、http.PostForm(url, data)http.Do(req)時棒搜,如變量值外部可控(指從參數中動態(tài)獲日铗取),應對請求目標進行嚴格的安全校驗力麸。

  • 如請求資源域名歸屬固定的范圍可款,如只允許a.qq.comb.qq.com,應做白名單限制克蚂。如不適用白名單闺鲸,則推薦的校驗邏輯步驟是:

    • 第 1 步、只允許HTTP或HTTPS協(xié)議

    • 第 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 步涩僻、如有跳轉缭召,跳轉后執(zhí)行1栈顷,否則綁定經校驗的ip和域名,對URL發(fā)起請求

  • 官方庫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)
    }

}

1.4 服務器端渲染

1.4.1【必須】模板渲染過濾驗證

  • 使用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 {
        // ...
    }
}

1.5 Web跨域

1.5.1【必須】跨域資源共享CORS限制請求來源

  • CORS請求保護不當可導致敏感信息泄漏晴竞,因此應當嚴格設置Access-Control-Allow-Origin使用同源策略進行保護蛙卤。
// 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響應包類型

  • 響應頭Content-Type與實際響應內容,應保持一致噩死。如:API響應數據類型是json颤难,則響應頭使用application/json;若為xml已维,則設置為text/xml行嗤。

1.6.2 【必須】添加安全響應頭

  • 所有接口、頁面垛耳,添加響應頭 X-Content-Type-Options: nosniff栅屏。
  • 所有接口、頁面堂鲜,添加響應頭X-Frame-Options栈雳。按需合理設置其允許范圍,包括:DENY缔莲、SAMEORIGIN哥纫、ALLOW-FROM origin。用法參考:MDN文檔

1.6.3【必須】外部輸入拼接到HTTP響應頭中需進行過濾

  • 應盡量避免外部可控參數拼接到HTTP響應頭中痴奏,如業(yè)務需要則需要過濾掉\r蛀骇、\n等換行符,或者拒絕攜帶換行符號的外部輸入读拆。

1.6.4【必須】外部輸入拼接到response頁面前進行編碼處理

  • 直出html頁面或使用模板生成html頁面的擅憔,推薦使用text/template自動編碼,或者使用html.EscapeStringtext/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)
}

1.7 會話管理

1.7.1【必須】安全維護session信息

  • 用戶登錄時應重新生成session暑诸,退出登錄后應清理session。
import (
    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
    "net/http"
)

// 創(chuàng)建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防護

  • 涉及系統(tǒng)敏感操作或可讀取敏感信息的接口應校驗Referer或添加csrf_token棉姐。
// 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【必須】默認鑒權

  • 除非資源完全可對外開放屠列,否則系統(tǒng)默認進行身份認證,使用白名單的方式放開不需要認證的接口或頁面伞矩。

  • 根據資源的機密程度和用戶角色,以最小權限原則夏志,設置不同級別的權限乃坤,如完全公開苛让、登錄可讀、登錄可寫湿诊、特定用戶可讀狱杰、特定用戶可寫等

  • 涉及用戶自身相關的數據的讀寫必須驗證登錄態(tài)用戶身份及其權限,避免越權操作

    -- 偽代碼
    select id from table where id=:id and userid=session.userid
    
  • 沒有獨立賬號體系的外網服務使用QQ微信登錄厅须,內網服務使用統(tǒng)一登錄服務登錄仿畸,其他使用賬號密碼登錄的服務需要增加驗證碼等二次驗證

1.9 并發(fā)保護

1.9.1【必須】禁止在閉包中直接調用循環(huán)變量

  • 在循環(huán)中啟動協(xié)程,當協(xié)程中使用到了循環(huán)的索引值朗和,由于多個協(xié)程同時使用同一個變量會產生數據競爭错沽,造成執(zhí)行結果異常。
// 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) // 把循環(huán)變量顯式地傳給協(xié)程
    }
    group.Wait()
}

1.9.2【必須】禁止并發(fā)寫map

  • 并發(fā)寫map容易造成程序崩潰并異常退出眶拉,建議加鎖保護
// bad
func main() {
    m := make(map[int]int)
    // 并發(fā)讀寫
    go func() {
        for {
            _ = m[1]
        }
    }()
    go func() {
        for {
            m[2] = 1
        }
    }()
    select {}
}

1.9.3【必須】確保并發(fā)安全

敏感操作如果未作并發(fā)安全限制千埃,可導致數據讀寫異常,造成業(yè)務邏輯限制被繞過忆植》趴桑可通過同步鎖或者原子操作進行防護。

通過同步鎖共享內存

// 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() // 交出時間片給協(xié)程
        if c > 10 {
            break
        }
    }
}
  • 使用sync/atomic執(zhí)行原子操作
// 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)      // 創(chuàng)建新值
        for k, v := range m1 {
            m2[k] = v
        }
        m2[key] = val
        m.Store(m2) // 用新的替代當前對象
    }
    _, _ = read, insert
}

引用:https://github.com/Tencent/secguide

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末耀里,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子拾氓,更是在濱河造成了極大的恐慌冯挎,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痪枫,死亡現場離奇詭異织堂,居然都是意外死亡,警方通過查閱死者的電腦和手機奶陈,發(fā)現死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門易阳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吃粒,你說我怎么就攤上這事潦俺。” “怎么了徐勃?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵事示,是天一觀的道長。 經常有香客問我僻肖,道長肖爵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任臀脏,我火速辦了婚禮劝堪,結果婚禮上冀自,老公的妹妹穿的比我還像新娘。我一直安慰自己秒啦,他們只是感情好熬粗,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著余境,像睡著了一般驻呐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芳来,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天含末,我揣著相機與錄音,去河邊找鬼绣张。 笑死答渔,一個胖子當著我的面吹牛,可吹牛的內容都是我干的侥涵。 我是一名探鬼主播沼撕,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芜飘!你這毒婦竟也來了务豺?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤嗦明,失蹤者是張志新(化名)和其女友劉穎笼沥,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體娶牌,經...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡奔浅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了诗良。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汹桦。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鉴裹,靈堂內的尸體忽然破棺而出舞骆,到底是詐尸還是另有隱情,我是刑警寧澤径荔,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布督禽,位于F島的核電站,受9級特大地震影響总处,放射性物質發(fā)生泄漏狈惫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一鹦马、第九天 我趴在偏房一處隱蔽的房頂上張望虱岂。 院中可真熱鬧玖院,春花似錦菠红、人聲如沸第岖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔑滓。三九已至,卻和暖如春遇绞,著一層夾襖步出監(jiān)牢的瞬間键袱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工摹闽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹄咖,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓付鹿,卻偏偏與公主長得像澜汤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子舵匾,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內容