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)go2. 命令执行#
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()go3. 文件上传和任意文件写入#
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)
}go4. 任意文件读取#
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)
}go5. 路径遍历#
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
}go6. 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)go7. 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)
}go8. 模板注入#
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)go9. 其他漏洞#
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