).MatchString(fl.Field().String())\n})\n\ntype Request struct {\n Name string `validate:\"required,safe_string\"`\n}\n```\n\n## Command Injection Prevention\n\n```go\nimport \"os/exec\"\n\n// SAFE - Use exec.Command with separate arguments\ncmd := exec.Command(\"ls\", \"-la\", directory)\noutput, err := cmd.Output()\n\n// SAFE - Use exec.CommandContext for timeout\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\ncmd := exec.CommandContext(ctx, \"ls\", \"-la\", directory)\n\n// UNSAFE - Shell expansion\ncmd := exec.Command(\"sh\", \"-c\", \"ls -la \" + directory) // NEVER with user input!\n\n// UNSAFE - Using os.system equivalent\n// Go doesn't have os.system, but avoid shell=true patterns\n```\n\n## Secure File Upload\n\n```go\nfunc UploadHandler(w http.ResponseWriter, r *http.Request) {\n // Limit request size\n r.Body = http.MaxBytesReader(w, r.Body, 10\u003c\u003c20) // 10 MB\n\n file, header, err := r.FormFile(\"file\")\n if err != nil {\n http.Error(w, \"File too large or invalid\", http.StatusBadRequest)\n return\n }\n defer file.Close()\n\n // Validate content type\n allowedTypes := map[string]bool{\n \"image/jpeg\": true,\n \"image/png\": true,\n \"application/pdf\": true,\n }\n\n buffer := make([]byte, 512)\n file.Read(buffer)\n contentType := http.DetectContentType(buffer)\n file.Seek(0, 0) // Reset reader\n\n if !allowedTypes[contentType] {\n http.Error(w, \"File type not allowed\", http.StatusBadRequest)\n return\n }\n\n // Generate safe filename\n ext := filepath.Ext(header.Filename)\n safeName := fmt.Sprintf(\"%s%s\", uuid.New().String(), ext)\n\n // Save outside web root\n destPath := filepath.Join(uploadDir, safeName)\n dest, err := os.Create(destPath)\n if err != nil {\n http.Error(w, \"Failed to save file\", http.StatusInternalServerError)\n return\n }\n defer dest.Close()\n\n io.Copy(dest, file)\n\n json.NewEncoder(w).Encode(map[string]string{\"filename\": safeName})\n}\n```\n\n## CORS Configuration\n\n### Gin\n\n```go\nimport \"github.com/gin-contrib/cors\"\n\nr := gin.Default()\n\nr.Use(cors.New(cors.Config{\n AllowOrigins: []string{\"https://myapp.com\"},\n AllowMethods: []string{\"GET\", \"POST\", \"PUT\", \"DELETE\"},\n AllowHeaders: []string{\"Authorization\", \"Content-Type\"},\n AllowCredentials: true,\n MaxAge: 12 * time.Hour,\n}))\n```\n\n### Standard Library\n\n```go\nfunc corsMiddleware(next http.Handler) http.Handler {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n origin := r.Header.Get(\"Origin\")\n allowedOrigins := map[string]bool{\"https://myapp.com\": true}\n\n if allowedOrigins[origin] {\n w.Header().Set(\"Access-Control-Allow-Origin\", origin)\n w.Header().Set(\"Access-Control-Allow-Credentials\", \"true\")\n }\n\n if r.Method == \"OPTIONS\" {\n w.Header().Set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE\")\n w.Header().Set(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type\")\n w.WriteHeader(http.StatusNoContent)\n return\n }\n\n next.ServeHTTP(w, r)\n })\n}\n```\n\n## Security Headers Middleware\n\n```go\nfunc securityHeaders(next http.Handler) http.Handler {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n w.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n w.Header().Set(\"X-Frame-Options\", \"DENY\")\n w.Header().Set(\"X-XSS-Protection\", \"0\") // Use CSP instead\n w.Header().Set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\")\n w.Header().Set(\"Content-Security-Policy\", \"default-src 'self'\")\n w.Header().Set(\"Strict-Transport-Security\", \"max-age=31536000; includeSubDomains\")\n\n next.ServeHTTP(w, r)\n })\n}\n```\n\n## Rate Limiting\n\n```go\nimport \"golang.org/x/time/rate\"\n\n// Per-IP rate limiter\ntype IPRateLimiter struct {\n ips map[string]*rate.Limiter\n mu *sync.RWMutex\n r rate.Limit\n b int\n}\n\nfunc NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {\n return &IPRateLimiter{\n ips: make(map[string]*rate.Limiter),\n mu: &sync.RWMutex{},\n r: r,\n b: b,\n }\n}\n\nfunc (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {\n i.mu.Lock()\n defer i.mu.Unlock()\n\n limiter, exists := i.ips[ip]\n if !exists {\n limiter = rate.NewLimiter(i.r, i.b)\n i.ips[ip] = limiter\n }\n\n return limiter\n}\n\n// Middleware\nfunc rateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {\n return func(c *gin.Context) {\n ip := c.ClientIP()\n if !limiter.GetLimiter(ip).Allow() {\n c.AbortWithStatusJSON(429, gin.H{\"error\": \"Too many requests\"})\n return\n }\n c.Next()\n }\n}\n```\n\n## Secrets Management\n\n```go\nimport \"os\"\n\n// Load from environment\ntype Config struct {\n JWTSecret string\n DatabaseURL string\n APIKey string\n}\n\nfunc LoadConfig() (*Config, error) {\n jwtSecret := os.Getenv(\"JWT_SECRET\")\n if jwtSecret == \"\" {\n return nil, errors.New(\"JWT_SECRET not set\")\n }\n\n return &Config{\n JWTSecret: jwtSecret,\n DatabaseURL: os.Getenv(\"DATABASE_URL\"),\n APIKey: os.Getenv(\"API_KEY\"),\n }, nil\n}\n\n// NEVER hardcode secrets\n// const jwtSecret = \"hardcoded-secret\" // NEVER!\n```\n\n## Logging Security Events\n\n```go\nimport \"log/slog\"\n\nfunc LogLoginAttempt(username string, success bool, ip string) {\n slog.Info(\"login attempt\",\n \"user\", username,\n \"success\", success,\n \"ip\", ip,\n )\n}\n\nfunc LogAccessDenied(userID string, resource string, ip string) {\n slog.Warn(\"access denied\",\n \"user_id\", userID,\n \"resource\", resource,\n \"ip\", ip,\n )\n}\n\n// NEVER log sensitive data\n// slog.Info(\"password\", \"value\", password) // NEVER!\n```\n\n## Anti-Patterns\n\n| Anti-Pattern | Why It's Bad | Correct Approach |\n|--------------|--------------|------------------|\n| `fmt.Sprintf` in SQL | SQL injection | Use parameterized queries |\n| `text/template` for HTML | XSS vulnerability | Use `html/template` |\n| Hardcoded secrets | Secret exposure | Use environment variables |\n| `exec.Command(\"sh\", \"-c\", input)` | Command injection | Use separate arguments |\n| Weak JWT signing | Token forgery | Use HS256 minimum, verify alg |\n| No request size limit | DoS attack | Use `MaxBytesReader` |\n| Using MD5/SHA1 for passwords | Easily cracked | Use bcrypt or argon2 |\n\n## Quick Troubleshooting\n\n| Issue | Likely Cause | Solution |\n|-------|--------------|----------|\n| govulncheck finds CVE | Vulnerable dependency | Update with `go get -u` |\n| JWT validation fails | Wrong signing method | Verify algorithm in ParseWithClaims |\n| CORS error | Origin not allowed | Add origin to allowed list |\n| bcrypt too slow | Cost factor too high | Use cost 10-12 |\n| File upload fails | Size limit exceeded | Increase MaxBytesReader limit |\n| Template not escaping | Using text/template | Switch to html/template |\n\n## Security Scanning Commands\n\n```bash\n# Vulnerability check\ngovulncheck ./...\n\n# Static analysis\nstaticcheck ./...\ngo vet ./...\n\n# Security linter\ngosec ./...\n\n# Dependency check\ngo list -m -json all | nancy sleuth\n\n# Snyk\nsnyk test\n\n# Check for secrets\ngitleaks detect\ntrufflehog git file://.\n```\n\n## Related Skills\n- [OWASP Top 10:2025](../owasp-top-10/SKILL.md)\n- [OWASP General](../owasp/SKILL.md)\n- [Secrets Management](../secrets-management/SKILL.md)\n- [Supply Chain Security](../supply-chain/SKILL.md)\n","licenseDocuments":[]}}
L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

go-security

go-security

Go security patterns for web applications. Covers dependency auditing, secure coding practices, crypto, and OWASP for Go ecosystem. USE WHEN: user works with "Go", "Golang", "Gin", "Fiber", "Echo", asks about "Go vulnerabilities", "Go modules security", "Go injection", "Go authentication" DO NOT USE FOR: general OWASP concepts - use `owasp` or `owasp-top-…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: go-security description: | Go security patterns for web applications. Covers dependency auditing, secure coding practices, crypto, and OWASP for Go ecosystem.

USE WHEN: user works with "Go", "Golang", "Gin", "Fiber", "Echo", asks about "Go vulnerabilities", "Go modules security", "Go injection", "Go authentication"

DO NOT USE FOR: general OWASP concepts - use owasp or owasp-top-10 instead, other language security - use language-specific skills allowed-tools: Read, Grep, Glob, Bash

Go Security - Quick Reference

When NOT to Use This Skill

  • General OWASP concepts - Use owasp or owasp-top-10 skill
  • Java security - Use java-security skill
  • Python security - Use python-security skill
  • Secrets management - Use secrets-management skill

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: go for Go security documentation.

Dependency Auditing

# Go built-in vulnerability check (Go 1.18+)
go list -m -json all | go run golang.org/x/vuln/cmd/govulncheck@latest

# govulncheck direct
govulncheck ./...

# Check for outdated modules
go list -u -m all

# Verify module checksums
go mod verify

# Snyk for Go
snyk test

CI/CD Integration

# GitHub Actions
- name: Security audit
  run: |
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck ./...

- name: Snyk scan
  uses: snyk/actions/golang@master
  with:
    args: --severity-threshold=high

SQL Injection Prevention

database/sql - Safe

// SAFE - Parameterized query with ?
row := db.QueryRow("SELECT * FROM users WHERE email = ?", email)

// SAFE - Parameterized query with $n (PostgreSQL)
row := db.QueryRow("SELECT * FROM users WHERE email = $1", email)

// SAFE - Named parameters with sqlx
row := db.NamedQuery("SELECT * FROM users WHERE email = :email",
    map[string]interface{}{"email": email})

database/sql - UNSAFE

// UNSAFE - String formatting
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)  // NEVER!
row := db.QueryRow(query)

// UNSAFE - String concatenation
query := "SELECT * FROM users WHERE email = '" + email + "'"  // NEVER!

GORM - Safe

// SAFE - GORM where clause
var user User
db.Where("email = ?", email).First(&user)

// SAFE - GORM struct condition
db.Where(&User{Email: email}).First(&user)

// SAFE - GORM map condition
db.Where(map[string]interface{}{"email": email}).First(&user)

GORM - UNSAFE

// UNSAFE - Raw with formatting
db.Raw(fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email))  // NEVER!

XSS Prevention

html/template (Auto-escaping)

// SAFE - html/template auto-escapes
import "html/template"

tmpl := template.Must(template.ParseFiles("page.html"))
tmpl.Execute(w, data)  // data.UserInput is auto-escaped
<!-- Template - auto-escaped -->
<p>{{.UserInput}}</p>

text/template - UNSAFE for HTML

// UNSAFE for HTML - text/template does NOT escape
import "text/template"  // Only for non-HTML content!

Manual Sanitization

import "html"

// Escape HTML entities
safeString := html.EscapeString(userInput)

// For rich HTML, use bluemonday
import "github.com/microcosm-cc/bluemonday"

p := bluemonday.UGCPolicy()
safeHTML := p.Sanitize(userInput)

Authentication - JWT

JWT with golang-jwt

import (
    "github.com/golang-jwt/jwt/v5"
    "time"
)

var jwtKey = []byte(os.Getenv("JWT_SECRET"))

type Claims struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    jwt.RegisteredClaims
}

func GenerateToken(userID, email string) (string, error) {
    claims := &Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

func ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}

    token, err := jwt.ParseWithClaims(tokenString, claims,
        func(token *jwt.Token) (interface{}, error) {
            // Validate signing method
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method")
            }
            return jwtKey, nil
        })

    if err != nil || !token.Valid {
        return nil, err
    }

    return claims, nil
}

Password Hashing with bcrypt

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

func HashPassword(password string) (string, error) {
    // Cost 12 is recommended
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    return string(bytes), err
}

func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

Password Hashing with Argon2

import "golang.org/x/crypto/argon2"

func HashPasswordArgon2(password string) (string, error) {
    salt := make([]byte, 16)
    if _, err := rand.Read(salt); err != nil {
        return "", err
    }

    hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)

    // Encode for storage
    return base64.StdEncoding.EncodeToString(append(salt, hash...)), nil
}

Input Validation

Using go-playground/validator

import "github.com/go-playground/validator/v10"

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email,max=255"`
    Password string `json:"password" validate:"required,min=12,max=128,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789,containsany=@$!%*?&"`
    Name     string `json:"name" validate:"required,min=2,max=100,alpha"`
}

var validate = validator.New()

func CreateUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    if err := validate.Struct(req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    // req is validated
}

Custom Validation

// Register custom validation
validate.RegisterValidation("safe_string", func(fl validator.FieldLevel) bool {
    return regexp.MustCompile(`^[a-zA-Z\s\-']+

  
    
    
    go-security · Skills-SI Market
    
    
    
    
    
    
    
    
  
  
    
).MatchString(fl.Field().String()) }) type Request struct { Name string `validate:"required,safe_string"` }

Command Injection Prevention

import "os/exec"

// SAFE - Use exec.Command with separate arguments
cmd := exec.Command("ls", "-la", directory)
output, err := cmd.Output()

// SAFE - Use exec.CommandContext for timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "ls", "-la", directory)

// UNSAFE - Shell expansion
cmd := exec.Command("sh", "-c", "ls -la " + directory)  // NEVER with user input!

// UNSAFE - Using os.system equivalent
// Go doesn't have os.system, but avoid shell=true patterns

Secure File Upload

func UploadHandler(w http.ResponseWriter, r *http.Request) {
    // Limit request size
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB

    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "File too large or invalid", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // Validate content type
    allowedTypes := map[string]bool{
        "image/jpeg":      true,
        "image/png":       true,
        "application/pdf": true,
    }

    buffer := make([]byte, 512)
    file.Read(buffer)
    contentType := http.DetectContentType(buffer)
    file.Seek(0, 0) // Reset reader

    if !allowedTypes[contentType] {
        http.Error(w, "File type not allowed", http.StatusBadRequest)
        return
    }

    // Generate safe filename
    ext := filepath.Ext(header.Filename)
    safeName := fmt.Sprintf("%s%s", uuid.New().String(), ext)

    // Save outside web root
    destPath := filepath.Join(uploadDir, safeName)
    dest, err := os.Create(destPath)
    if err != nil {
        http.Error(w, "Failed to save file", http.StatusInternalServerError)
        return
    }
    defer dest.Close()

    io.Copy(dest, file)

    json.NewEncoder(w).Encode(map[string]string{"filename": safeName})
}

CORS Configuration

Gin

import "github.com/gin-contrib/cors"

r := gin.Default()

r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://myapp.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Authorization", "Content-Type"},
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}))

Standard Library

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        allowedOrigins := map[string]bool{"https://myapp.com": true}

        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Credentials", "true")
        }

        if r.Method == "OPTIONS" {
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Security Headers Middleware

func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "0") // Use CSP instead
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")

        next.ServeHTTP(w, r)
    })
}

Rate Limiting

import "golang.org/x/time/rate"

// Per-IP rate limiter
type IPRateLimiter struct {
    ips map[string]*rate.Limiter
    mu  *sync.RWMutex
    r   rate.Limit
    b   int
}

func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
    return &IPRateLimiter{
        ips: make(map[string]*rate.Limiter),
        mu:  &sync.RWMutex{},
        r:   r,
        b:   b,
    }
}

func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    i.mu.Lock()
    defer i.mu.Unlock()

    limiter, exists := i.ips[ip]
    if !exists {
        limiter = rate.NewLimiter(i.r, i.b)
        i.ips[ip] = limiter
    }

    return limiter
}

// Middleware
func rateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        ip := c.ClientIP()
        if !limiter.GetLimiter(ip).Allow() {
            c.AbortWithStatusJSON(429, gin.H{"error": "Too many requests"})
            return
        }
        c.Next()
    }
}

Secrets Management

import "os"

// Load from environment
type Config struct {
    JWTSecret    string
    DatabaseURL  string
    APIKey       string
}

func LoadConfig() (*Config, error) {
    jwtSecret := os.Getenv("JWT_SECRET")
    if jwtSecret == "" {
        return nil, errors.New("JWT_SECRET not set")
    }

    return &Config{
        JWTSecret:   jwtSecret,
        DatabaseURL: os.Getenv("DATABASE_URL"),
        APIKey:      os.Getenv("API_KEY"),
    }, nil
}

// NEVER hardcode secrets
// const jwtSecret = "hardcoded-secret"  // NEVER!

Logging Security Events

import "log/slog"

func LogLoginAttempt(username string, success bool, ip string) {
    slog.Info("login attempt",
        "user", username,
        "success", success,
        "ip", ip,
    )
}

func LogAccessDenied(userID string, resource string, ip string) {
    slog.Warn("access denied",
        "user_id", userID,
        "resource", resource,
        "ip", ip,
    )
}

// NEVER log sensitive data
// slog.Info("password", "value", password)  // NEVER!

Anti-Patterns

Anti-PatternWhy It's BadCorrect Approach
fmt.Sprintf in SQLSQL injectionUse parameterized queries
text/template for HTMLXSS vulnerabilityUse html/template
Hardcoded secretsSecret exposureUse environment variables
exec.Command("sh", "-c", input)Command injectionUse separate arguments
Weak JWT signingToken forgeryUse HS256 minimum, verify alg
No request size limitDoS attackUse MaxBytesReader
Using MD5/SHA1 for passwordsEasily crackedUse bcrypt or argon2

Quick Troubleshooting

IssueLikely CauseSolution
govulncheck finds CVEVulnerable dependencyUpdate with go get -u
JWT validation failsWrong signing methodVerify algorithm in ParseWithClaims
CORS errorOrigin not allowedAdd origin to allowed list
bcrypt too slowCost factor too highUse cost 10-12
File upload failsSize limit exceededIncrease MaxBytesReader limit
Template not escapingUsing text/templateSwitch to html/template

Security Scanning Commands

# Vulnerability check
govulncheck ./...

# Static analysis
staticcheck ./...
go vet ./...

# Security linter
gosec ./...

# Dependency check
go list -m -json all | nancy sleuth

# Snyk
snyk test

# Check for secrets
gitleaks detect
trufflehog git file://.

Related Skills

Skills enregistrées