---
title: "Securing Go APIs: OWASP Best Practices"
url: "https://vanducng.dev/2024/05/03/Securing-Go-APIs-OWASP-Best-Practices/"
date: "2024-05-03"
series: "go-backend"
categories: ["Backend"]
tags: ["go","security","api"]
---

# Securing Go APIs: OWASP Best Practices

Security isn't an afterthought - it's a fundamental requirement for any production API. This post covers the OWASP API Security Top 10 vulnerabilities and practical Go implementations to protect your applications.

## OWASP API Security Top 10

```mermaid
flowchart TD
    subgraph Top10["OWASP API Security Top 10"]
        A1[1. Broken Object Level Auth]
        A2[2. Broken Authentication]
        A3[3. Broken Object Property Auth]
        A4[4. Unrestricted Resource Consumption]
        A5[5. Broken Function Level Auth]
        A6[6. Mass Assignment]
        A7[7. Security Misconfiguration]
        A8[8. Injection]
        A9[9. Improper Asset Management]
        A10[10. Unsafe API Consumption]
    end

    classDef pinkClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff

    class Top10 pinkClass
```

## SQL Injection Prevention

### The Vulnerability

```go
// VULNERABLE - Never do this!
func GetUser(db *sql.DB, username string) (*User, error) {
    query := "SELECT * FROM users WHERE username = '" + username + "'"
    // Attacker input: ' OR '1'='1
    // Results in: SELECT * FROM users WHERE username = '' OR '1'='1'
    row := db.QueryRow(query)
    // ...
}
```

### Secure Implementation

```go
// Using parameterized queries (GORM)
func GetUser(db *gorm.DB, username string) (*User, error) {
    var user User
    if err := db.Where("username = ?", username).First(&user).Error; err != nil {
        return nil, err
    }
    return &user, nil
}

// Using parameterized queries (database/sql)
func GetUserSQL(db *sql.DB, username string) (*User, error) {
    query := "SELECT id, username, email FROM users WHERE username = $1"
    row := db.QueryRow(query, username)

    var user User
    if err := row.Scan(&user.ID, &user.Username, &user.Email); err != nil {
        return nil, err
    }
    return &user, nil
}

// Safe dynamic queries with validation
func SearchProducts(db *gorm.DB, filters map[string]string) ([]Product, error) {
    allowedFields := map[string]bool{
        "name":     true,
        "category": true,
        "status":   true,
    }

    query := db.Model(&Product{})

    for field, value := range filters {
        if !allowedFields[field] {
            continue // Skip unknown fields
        }
        query = query.Where(field+" = ?", value)
    }

    var products []Product
    return products, query.Find(&products).Error
}
```

## Input Validation

### Validation Middleware

```go
import (
    "github.com/go-playground/validator/v10"
)

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,containsany=!@#$%"`
    Age      int    `json:"age" validate:"omitempty,min=13,max=120"`
}

var validate = validator.New()

func ValidateRequest(req interface{}) error {
    if err := validate.Struct(req); err != nil {
        var validationErrors []string
        for _, err := range err.(validator.ValidationErrors) {
            validationErrors = append(validationErrors,
                fmt.Sprintf("%s: %s", err.Field(), err.Tag()))
        }
        return fmt.Errorf("validation failed: %v", validationErrors)
    }
    return nil
}

// Sanitize input
func SanitizeString(input string) string {
    // Remove potential XSS characters
    input = html.EscapeString(input)
    // Trim whitespace
    input = strings.TrimSpace(input)
    return input
}
```

### Content Type Validation

```go
func ContentTypeMiddleware(allowedTypes ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" {
            contentType := c.ContentType()

            allowed := false
            for _, t := range allowedTypes {
                if strings.HasPrefix(contentType, t) {
                    allowed = true
                    break
                }
            }

            if !allowed {
                c.JSON(http.StatusUnsupportedMediaType, gin.H{
                    "error": "Content-Type not allowed",
                })
                c.Abort()
                return
            }
        }
        c.Next()
    }
}

// Usage
router.Use(ContentTypeMiddleware("application/json"))
```

## Mass Assignment Protection

### The Vulnerability

```go
// VULNERABLE - Allows attackers to modify any field
func UpdateUser(c *gin.Context) {
    var user User
    db.First(&user, c.Param("id"))

    // Attacker could send: {"role": "admin", "is_active": false}
    c.ShouldBindJSON(&user)
    db.Save(&user)
}
```

### Secure Implementation

```go
// Define explicit update DTOs
type UpdateUserRequest struct {
    Username string `json:"username" binding:"omitempty,min=3,max=50"`
    Email    string `json:"email" binding:"omitempty,email"`
    Bio      string `json:"bio" binding:"omitempty,max=500"`
    // Note: role and is_active are NOT included
}

func UpdateUser(c *gin.Context) {
    id := c.Param("id")

    var req UpdateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Only update allowed fields
    updates := map[string]interface{}{}
    if req.Username != "" {
        updates["username"] = req.Username
    }
    if req.Email != "" {
        updates["email"] = req.Email
    }
    if req.Bio != "" {
        updates["bio"] = req.Bio
    }

    if len(updates) == 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "No fields to update"})
        return
    }

    if err := db.Model(&User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Update failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "Updated successfully"})
}
```

## Security Headers

```go
func SecurityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Prevent clickjacking
        c.Header("X-Frame-Options", "DENY")

        // Prevent MIME type sniffing
        c.Header("X-Content-Type-Options", "nosniff")

        // Enable XSS filter
        c.Header("X-XSS-Protection", "1; mode=block")

        // Strict Transport Security
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")

        // Content Security Policy
        c.Header("Content-Security-Policy", "default-src 'self'")

        // Referrer Policy
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")

        // Permissions Policy
        c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")

        c.Next()
    }
}
```

## Rate Limiting

### Token Bucket with Redis

```go
import (
    "context"
    "time"

    "github.com/redis/go-redis/v9"
)

type RateLimiter struct {
    client     *redis.Client
    rate       int           // Requests per window
    window     time.Duration // Window size
}

func NewRateLimiter(client *redis.Client, rate int, window time.Duration) *RateLimiter {
    return &RateLimiter{
        client: client,
        rate:   rate,
        window: window,
    }
}

func (rl *RateLimiter) Allow(ctx context.Context, key string) (bool, int, error) {
    now := time.Now()
    windowStart := now.Truncate(rl.window)
    redisKey := fmt.Sprintf("ratelimit:%s:%d", key, windowStart.Unix())

    pipe := rl.client.Pipeline()
    incr := pipe.Incr(ctx, redisKey)
    pipe.Expire(ctx, redisKey, rl.window)
    _, err := pipe.Exec(ctx)

    if err != nil {
        return false, 0, err
    }

    count := int(incr.Val())
    remaining := rl.rate - count
    if remaining < 0 {
        remaining = 0
    }

    return count <= rl.rate, remaining, nil
}

func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := c.ClientIP() // Or use API key, user ID

        allowed, remaining, err := limiter.Allow(c.Request.Context(), key)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Rate limit error"})
            c.Abort()
            return
        }

        c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
        c.Header("X-RateLimit-Limit", strconv.Itoa(limiter.rate))

        if !allowed {
            c.Header("Retry-After", strconv.Itoa(int(limiter.window.Seconds())))
            c.JSON(http.StatusTooManyRequests, gin.H{
                "error": "Rate limit exceeded",
            })
            c.Abort()
            return
        }

        c.Next()
    }
}
```

## Structured Logging

```go
import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func InitLogger() *zap.Logger {
    config := zap.Config{
        Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
        Development: false,
        Encoding:    "json",
        EncoderConfig: zapcore.EncoderConfig{
            TimeKey:        "timestamp",
            LevelKey:       "level",
            NameKey:        "logger",
            CallerKey:      "caller",
            MessageKey:     "message",
            StacktraceKey:  "stacktrace",
            LineEnding:     zapcore.DefaultLineEnding,
            EncodeLevel:    zapcore.LowercaseLevelEncoder,
            EncodeTime:     zapcore.ISO8601TimeEncoder,
            EncodeDuration: zapcore.SecondsDurationEncoder,
            EncodeCaller:   zapcore.ShortCallerEncoder,
        },
        OutputPaths:      []string{"stdout"},
        ErrorOutputPaths: []string{"stderr"},
    }

    logger, _ := config.Build()
    return logger
}

func LoggingMiddleware(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery

        c.Next()

        latency := time.Since(start)

        // Security: Don't log sensitive data
        fields := []zap.Field{
            zap.Int("status", c.Writer.Status()),
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.String("ip", c.ClientIP()),
            zap.Duration("latency", latency),
            zap.String("user_agent", c.Request.UserAgent()),
        }

        if query != "" {
            // Sanitize query params (remove tokens, passwords)
            fields = append(fields, zap.String("query", sanitizeQuery(query)))
        }

        if requestID := c.GetString("request_id"); requestID != "" {
            fields = append(fields, zap.String("request_id", requestID))
        }

        if userID := c.GetUint("user_id"); userID > 0 {
            fields = append(fields, zap.Uint("user_id", userID))
        }

        if len(c.Errors) > 0 {
            fields = append(fields, zap.String("errors", c.Errors.String()))
            logger.Error("Request failed", fields...)
        } else {
            logger.Info("Request completed", fields...)
        }
    }
}

func sanitizeQuery(query string) string {
    // Remove sensitive parameters
    sensitiveParams := []string{"password", "token", "api_key", "secret"}
    for _, param := range sensitiveParams {
        re := regexp.MustCompile(fmt.Sprintf(`(%s=)[^&]*`, param))
        query = re.ReplaceAllString(query, "${1}[REDACTED]")
    }
    return query
}
```

## Security Audit Logging

```go
type AuditLog struct {
    ID          uint      `json:"id" gorm:"primaryKey"`
    UserID      uint      `json:"user_id"`
    Action      string    `json:"action"`
    Resource    string    `json:"resource"`
    ResourceID  string    `json:"resource_id"`
    IPAddress   string    `json:"ip_address"`
    UserAgent   string    `json:"user_agent"`
    Status      string    `json:"status"` // success, failure
    Details     string    `json:"details" gorm:"type:jsonb"`
    CreatedAt   time.Time `json:"created_at"`
}

type AuditLogger struct {
    db *gorm.DB
}

func (a *AuditLogger) Log(c *gin.Context, action, resource, resourceID, status string, details map[string]interface{}) {
    detailsJSON, _ := json.Marshal(details)

    log := AuditLog{
        UserID:     c.GetUint("user_id"),
        Action:     action,
        Resource:   resource,
        ResourceID: resourceID,
        IPAddress:  c.ClientIP(),
        UserAgent:  c.Request.UserAgent(),
        Status:     status,
        Details:    string(detailsJSON),
    }

    go a.db.Create(&log) // Async logging
}

// Usage in handlers
func (h *Handler) DeleteUser(c *gin.Context) {
    userID := c.Param("id")

    if err := h.userRepo.Delete(userID); err != nil {
        h.audit.Log(c, "delete", "user", userID, "failure", map[string]interface{}{
            "error": err.Error(),
        })
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Delete failed"})
        return
    }

    h.audit.Log(c, "delete", "user", userID, "success", nil)
    c.Status(http.StatusNoContent)
}
```

## HTTPS and TLS

```go
import (
    "crypto/tls"
)

func main() {
    router := gin.Default()
    // ... setup routes ...

    // TLS configuration
    tlsConfig := &tls.Config{
        MinVersion:               tls.VersionTLS12,
        PreferServerCipherSuites: true,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        },
    }

    server := &http.Server{
        Addr:      ":443",
        Handler:   router,
        TLSConfig: tlsConfig,
    }

    // HTTP to HTTPS redirect
    go func() {
        redirectServer := &http.Server{
            Addr: ":80",
            Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                target := "https://" + r.Host + r.URL.Path
                http.Redirect(w, r, target, http.StatusMovedPermanently)
            }),
        }
        redirectServer.ListenAndServe()
    }()

    log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}
```

## Security Checklist

```mermaid
flowchart TD
    subgraph Input["Input Security"]
        I1[Parameterized Queries]
        I2[Input Validation]
        I3[Content-Type Check]
    end

    subgraph Auth["Authentication"]
        A1[Strong Password Hash]
        A2[JWT with Short Expiry]
        A3[Secure Token Storage]
    end

    subgraph Access["Access Control"]
        AC1[RBAC/ABAC]
        AC2[Resource Ownership]
        AC3[Mass Assignment Protection]
    end

    subgraph Monitor["Monitoring"]
        M1[Structured Logging]
        M2[Audit Trail]
        M3[Rate Limiting]
    end

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
    classDef pinkClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff

    class Input blueClass
    class Auth orangeClass
    class Access greenClass
    class Monitor pinkClass
```

## Summary

| Area | Best Practices |
|------|----------------|
| **Injection** | Parameterized queries, input validation |
| **Authentication** | bcrypt, JWT, secure token storage |
| **Authorization** | RBAC/ABAC, ownership checks |
| **Data Protection** | Mass assignment prevention, field whitelisting |
| **Transport** | HTTPS only, TLS 1.2+ |
| **Headers** | Security headers (CSP, HSTS, X-Frame-Options) |
| **Monitoring** | Structured logging, audit trails |
| **Rate Limiting** | Token bucket, per-client limits |

---

*This concludes the Go Backend Development series. For the complete learning path, see the [Series Overview](https://vanducng.dev/2024/03/01/Why-Go-for-Backend-Development/).*
