1. SQL注入#

Sink点#

  • database/sql.DB.Query()
  • database/sql.DB.QueryRow()
  • database/sql.DB.Exec()
  • database/sql.Stmt.Query()
  • database/sql.Stmt.QueryRow()
  • database/sql.Stmt.Exec()
  • GORM Where()
  • GORM Raw()
  • GORM Order()

审计检查项#

  • 是否使用?占位符进行参数化
  • SQL语句中是否存在字符串拼接
  • 用户输入是否直接进入SQL
  • 是否使用Prepare()进行预处理
  • GORM Where()是否参数化形式
  • GORM Raw()是否与用户拼接
  • GORM Order()是否有白名单
  • 无法参数化部分是否验证

风险代码模式#

// 模式1:字符串拼接
username := r.FormValue("username")
query := "SELECT * FROM users WHERE username = '" + username + "'"
rows, err := db.Query(query)

go

// 模式2:GORM Raw拼接
filter := r.FormValue("filter")
var user User
db.Raw("SELECT * FROM users WHERE username = '" + filter + "'").Scan(&user)

go

// 模式3:GORM Where拼接
db.Where("username = '" + username + "'").First(&user)

go

// 模式4:ORDER BY未验证
orderBy := r.FormValue("orderBy")
db.Order(orderBy).Find(&users)

go

安全实现#

// 方案1:参数化查询
username := r.FormValue("username")
var user User
err := db.QueryRow(
    "SELECT id, username FROM users WHERE username = ?",
    username,
).Scan(&user.ID, &user.Username)

go

// 方案2:GORM参数化
db.Where("username = ?", username).First(&user)

go

// 方案3:GORM Order白名单
orderBy := r.FormValue("orderBy")
allowedFields := map[string]bool{
    "id": true, "username": true, "email": true, "created_at": true,
}
if !allowedFields[orderBy] {
    http.Error(w, "Invalid field", 400)
    return
}
db.Order(orderBy).Find(&users)

go

// 方案4:Prepare预处理
stmt, err := db.Prepare("SELECT * FROM users WHERE username = ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

rows, err := stmt.Query(username)

go


2. 命令执行#

Sink点#

  • os/exec.Command()
  • os/exec.CommandContext()
  • os/exec.LookPath()

审计检查项#

  • 第二个参数(args)是否包含用户输入
  • 第一个参数(程序名)是否来自用户
  • 是否使用了绝对路径
  • 是否实现了命令白名单
  • 参数是否进行了验证

风险代码模式#

// 模式1:用户输入直接作为命令
cmd := r.FormValue("cmd")
out, err := exec.Command(cmd).CombinedOutput()

go

// 模式2:通过shell执行用户输入
cmd := r.FormValue("cmd")
out, err := exec.Command("sh", "-c", cmd).CombinedOutput()

go

// 模式3:从用户输入确定可执行程序
program := r.FormValue("program")
out, err := exec.Command(program, "arg").CombinedOutput()

go

// 模式4:参数包含用户输入但未验证
hostname := r.FormValue("host")
out, err := exec.Command("ping", "-c", "4", hostname).CombinedOutput()

go

安全实现#

// 方案1:命令白名单
cmd := r.FormValue("cmd")
allowedCommands := map[string]bool{
    "whoami": true,
    "date":   true,
    "pwd":    true,
}

if !allowedCommands[cmd] {
    http.Error(w, "Command not allowed", 400)
    return
}

out, err := exec.Command(cmd).CombinedOutput()
fmt.Fprint(w, string(out))

go

// 方案2:参数白名单+绝对路径
hostname := r.FormValue("host")
allowedHosts := map[string]bool{
    "google.com":  true,
    "github.com":  true,
    "example.com": true,
}

if !allowedHosts[hostname] {
    http.Error(w, "Host not allowed", 400)
    return
}

cmd := exec.Command("/bin/ping", "-c", "4", hostname)
cmd.Stdout = w
cmd.Stderr = w
err := cmd.Run()

go


3. 文件上传和任意文件写入#

Sink点#

  • os.Create()
  • os.OpenFile()
  • ioutil.WriteFile()
  • os.WriteFile()
  • io.Copy()

审计检查项#

  • 扩展名是否进行白名单验证
  • 是否防止了.go.exe等文件
  • MIME类型是否经过验证
  • 文件名是否包含路径分隔符
  • 是否生成了新文件名
  • 最终路径是否在预期目录内

风险代码模式#

// 模式1:直接使用上传文件名
file, header, _ := r.FormFile("upload")
defer file.Close()

dst, _ := os.Create("uploads/" + header.Filename)
io.Copy(dst, file)

go

// 模式2:仅检查扩展名
if !strings.HasSuffix(header.Filename, ".jpg") {
    return
}
// 但shell.jpg.go可绕过

go

安全实现#

import (
    "crypto/md5"
    "fmt"
    "io"
    "path/filepath"
    "strings"
)

func handleFileUpload(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(10 * 1024 * 1024)
    
    file, header, err := r.FormFile("upload")
    if err != nil {
        http.Error(w, "Upload error", 400)
        return
    }
    defer file.Close()
    
    ext := filepath.Ext(header.Filename)
    allowedExts := map[string]bool{
        ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
    }
    
    if !allowedExts[strings.ToLower(ext)] {
        http.Error(w, "File type not allowed", 400)
        return
    }
    
    buf := make([]byte, 512)
    n, _ := file.Read(buf)
    mimeType := http.DetectContentType(buf[:n])
    file.Seek(0, 0)
    
    allowedMimes := map[string]bool{
        "image/jpeg": true,
        "image/png":  true,
        "image/gif":  true,
    }
    
    if !allowedMimes[mimeType] {
        http.Error(w, "Invalid MIME type", 400)
        return
    }
    
    hash := md5.Sum(buf[:n])
    filename := fmt.Sprintf("%x%s", hash, ext)
    
    uploadDir := "uploads"
    filepath := filepath.Join(uploadDir, filename)
    absPath, _ := filepath.Abs(filepath)
    absDir, _ := filepath.Abs(uploadDir)
    
    if !strings.HasPrefix(absPath, absDir) {
        http.Error(w, "Invalid path", 400)
        return
    }
    
    dst, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        http.Error(w, "Save error", 500)
        return
    }
    defer dst.Close()
    
    io.Copy(dst, file)
    w.WriteHeader(http.StatusOK)
}

go


4. 任意文件读取#

Sink点#

  • os.Open()
  • ioutil.ReadFile()
  • os.ReadFile()
  • io.ReadAll()
  • bufio.Scanner

审计检查项#

  • 用户参数是否直接作为文件路径
  • 是否使用filepath.Base()
  • 是否使用filepath.Abs()
  • 最终路径是否在允许目录内

安全实现#

func readFileSafely(w http.ResponseWriter, r *http.Request) {
    filename := r.FormValue("file")
    
    safeName := filepath.Base(filename)
    baseDir := "files"
    filepath := filepath.Join(baseDir, safeName)
    absPath, _ := filepath.Abs(filepath)
    absBaseDir, _ := filepath.Abs(baseDir)
    
    if !strings.HasPrefix(absPath, absBaseDir) {
        http.Error(w, "Access denied", 403)
        return
    }
    
    content, err := ioutil.ReadFile(filepath)
    if err != nil {
        http.Error(w, "File not found", 404)
        return
    }
    
    w.Header().Set("Content-Type", "text/plain")
    w.Write(content)
}

go


5. 路径遍历#

Sink点#

  • 文件操作

审计检查项#

  • 路径是否包含用户输入
  • 是否使用filepath.Clean()
  • 规范化后路径是否在基础目录内

安全实现#

func validatePath(userPath string) (string, error) {
    baseDir := "downloads"
    filepath := filepath.Join(baseDir, userPath)
    filepath = filepath.Clean(filepath)
    
    absPath, _ := filepath.Abs(filepath)
    absBaseDir, _ := filepath.Abs(baseDir)
    
    if !strings.HasPrefix(absPath, absBaseDir) {
        return "", fmt.Errorf("path traversal detected")
    }
    
    return absPath, nil
}

go


6. XXE (XML External Entity)#

Sink点#

  • encoding/xml.Unmarshal()
  • encoding/xml.NewDecoder()
  • encoding/xml.Decoder.Decode()

审计检查项#

  • 是否对不可信XML进行解析
  • 是否禁用了外部实体
  • DOCTYPE声明是否被检查

说明:Go的encoding/xml默认不解析外部实体,相对安全。

安全实现#

xmlData := r.FormValue("xml")

// 可选:检查并拒绝DOCTYPE
if strings.Contains(xmlData, "<!DOCTYPE") {
    http.Error(w, "DOCTYPE not allowed", 400)
    return
}

decoder := xml.NewDecoder(strings.NewReader(xmlData))
var data MyStruct
err := decoder.Decode(&data)

go


7. SSRF (Server-Side Request Forgery)#

Sink点#

  • net.Dial()
  • http.Get()
  • http.Post()
  • http.Client.Do()
  • net.LookupIP()

审计检查项#

  • 是否允许用户指定URL
  • 是否检测内部地址(127.0.0.1)
  • 是否限制了协议(仅HTTP/HTTPS)
  • 是否防止了DNS重绑定
  • 域名是否在白名单内

安全实现#

import (
    "net"
    "net/url"
)

func isPrivateIP(ip string) bool {
    parsedIP := net.ParseIP(ip)
    return parsedIP.IsLoopback() || parsedIP.IsPrivate() || parsedIP.IsLinkLocalUnicast()
}

func validateURL(urlStr string) error {
    parsedURL, err := url.Parse(urlStr)
    if err != nil {
        return err
    }
    
    if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
        return fmt.Errorf("invalid scheme: %s", parsedURL.Scheme)
    }
    
    ips, err := net.LookupIP(parsedURL.Hostname())
    if err != nil {
        return err
    }
    
    for _, ip := range ips {
        if isPrivateIP(ip.String()) {
            return fmt.Errorf("private IP address not allowed")
        }
    }
    
    whitelist := map[string]bool{
        "example.com":     true,
        "api.example.com": true,
    }
    
    if !whitelist[parsedURL.Hostname()] {
        return fmt.Errorf("domain not in whitelist")
    }
    
    return nil
}

func handleFetch(w http.ResponseWriter, r *http.Request) {
    urlStr := r.FormValue("url")
    
    if err := validateURL(urlStr); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(urlStr)
    if err != nil {
        http.Error(w, "Fetch error", 500)
        return
    }
    defer resp.Body.Close()
    
    io.Copy(w, resp.Body)
}

go


8. 模板注入#

Sink点#

  • text/template.Parse()
  • html/template.Parse()
  • text/template.Execute()
  • html/template.Execute()

审计检查项#

  • 是否允许用户编辑模板
  • 是否使用text/template
  • 是否使用html/template
  • 是否将用户输入作为模板

风险代码模式#

// 模式1:用户可控制模板
tplContent := r.FormValue("template")
tpl, _ := template.New("user").Parse(tplContent)
tpl.Execute(w, data)

go

// 模式2:使用text/template(无XSS防护)
import "text/template"
// <script> 会直接输出

go

安全实现#

import "html/template"

// 方案1:从文件加载
tpl, _ := html.template.ParseFiles("templates/index.html")
tpl.Execute(w, data)

// 方案2:使用html/template(自动转义)
tpl, _ := html.template.New("safe").Parse("<div>{{.UserInput}}</div>")
// UserInput中的<script>会被转义

// 方案3:显式转义
import "html"
safeTxt := html.EscapeString(userInput)
fmt.Fprintf(w, "<div>%s</div>", safeTxt)

go


9. 其他漏洞#

XSS#

Sink点fmt.Fprint()

检查项:使用html/template自动转义

开放重定向#

Sink点http.Redirect()

检查项:验证重定向URL


10. Go语言安全特性#

特性说明
强类型编译编译期类型检查
默认XML安全encoding/xml不解析外部实体
反序列化安全不自动调用方法
内存安全垃圾回收机制

注意事项#

  • Windows下os/exec的PATH搜索问题
  • text/template缺乏XSS防护,应使用html/template
  • 第三方库可能有漏洞

11. 审计工具#

  • 静态分析:golangci-lint、gosec、SonarQube、Semgrep
  • 动态检测:OWASP ZAP、Burp Suite
  • 依赖检查:go mod audit(Go 1.21+)、nancy、Snyk
  • 官方工具:govulncheck