Back to all posts
December 20, 2025Charlie BrownSecurity

Golang Security Best Practices: Building Secure Applications

Learn essential security practices for Go applications including input validation, SQL injection prevention, authentication, and secure API design.

Golang Security Best Practices: Building Secure Applications

Golang Security Best Practices: Building Secure Applications

Go is known for its simplicity and performance, but building secure applications requires understanding common vulnerabilities and implementing proper security measures. In this article, we'll explore essential security practices for Go applications, covering input validation, SQL injection prevention, authentication, and secure API design.

Input Validation and Sanitization

1. Validate All Input

Never trust user input. Always validate and sanitize:

go
// main.go
package main

import (
    "fmt"
    "net/http"
    "regexp"
    "strings"
    
    "github.com/go-playground/validator/v10"
)

type UserInput struct {
    Email    string `json:"email" validate:"required,email"`
    Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
}

var validate = validator.New()

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var input UserInput
    
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    // Validate input
    if err := validate.Struct(input); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Additional custom validation
    if !isValidEmail(input.Email) {
        http.Error(w, "Invalid email format", http.StatusBadRequest)
        return
    }
    
    // Sanitize input
    input.Username = sanitizeString(input.Username)
    
    // Process user creation
    // ...
}

func isValidEmail(email string) bool {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    return emailRegex.MatchString(email)
}

func sanitizeString(s string) string {
    // Remove potentially dangerous characters
    s = strings.TrimSpace(s)
    s = strings.ReplaceAll(s, "<", "&lt;")
    s = strings.ReplaceAll(s, ">", "&gt;")
    return s
}

2. Use Whitelist Validation

Prefer whitelist over blacklist validation:

go
// ✅ Good: Whitelist approach
func isValidRole(role string) bool {
    validRoles := map[string]bool{
        "admin":    true,
        "user":     true,
        "moderator": true,
    }
    return validRoles[strings.ToLower(role)]
}

// ❌ Bad: Blacklist approach
func isValidRole(role string) bool {
    invalidRoles := []string{"hacker", "malicious"}
    for _, invalid := range invalidRoles {
        if strings.Contains(role, invalid) {
            return false
        }
    }
    return true
}

SQL Injection Prevention

1. Use Parameterized Queries

Always use parameterized queries with database/sql:

go
// ✅ Good: Parameterized query
func getUserByEmail(db *sql.DB, email string) (*User, error) {
    query := "SELECT id, email, name FROM users WHERE email = $1"
    row := db.QueryRow(query, email)
    
    var user User
    err := row.Scan(&user.ID, &user.Email, &user.Name)
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

// ❌ Bad: String concatenation (SQL injection vulnerability)
func getUserByEmailBad(db *sql.DB, email string) (*User, error) {
    query := fmt.Sprintf("SELECT id, email, name FROM users WHERE email = '%s'", email)
    // Vulnerable to SQL injection!
    row := db.QueryRow(query)
    // ...
}

2. Use GORM Safely

When using GORM, use parameterized queries:

go
// ✅ Good: GORM parameterized query
func getUserByEmail(db *gorm.DB, email string) (*User, error) {
    var user User
    result := db.Where("email = ?", email).First(&user)
    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

// ❌ Bad: GORM with string interpolation
func getUserByEmailBad(db *gorm.DB, email string) (*User, error) {
    var user User
    // Vulnerable!
    result := db.Where(fmt.Sprintf("email = '%s'", email)).First(&user)
    return &user, result.Error
}

Authentication and Authorization

1. Secure Password Hashing

Use bcrypt or argon2 for password hashing:

go
// auth/password.go
package auth

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

const bcryptCost = 12

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

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

2. JWT Token Implementation

Implement secure JWT tokens:

go
// auth/jwt.go
package auth

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

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

var jwtSecret = []byte("your-secret-key") // Use environment variable!

func GenerateToken(userID, email, role string) (string, error) {
    expirationTime := time.Now().Add(15 * time.Minute)
    
    claims := &Claims{
        UserID: userID,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "your-app",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}
    
    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }
    
    return claims, nil
}

3. Middleware for Authentication

Create authentication middleware:

go
// middleware/auth.go
package middleware

import (
    "net/http"
    "strings"
    
    "your-app/auth"
)

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Authorization header required", http.StatusUnauthorized)
            return
        }
        
        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        if tokenString == authHeader {
            http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
            return
        }
        
        claims, err := auth.ValidateToken(tokenString)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        // Add user info to request context
        ctx := context.WithValue(r.Context(), "userID", claims.UserID)
        ctx = context.WithValue(ctx, "userRole", claims.Role)
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Secure API Design

1. Rate Limiting

Implement rate limiting to prevent abuse:

go
// middleware/ratelimit.go
package middleware

import (
    "net/http"
    "sync"
    "time"
)

type rateLimiter struct {
    requests map[string][]time.Time
    mutex    sync.Mutex
    limit    int
    window   time.Duration
}

var limiter = &rateLimiter{
    requests: make(map[string][]time.Time),
    limit:    100,
    window:   time.Minute,
}

func RateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := getClientIP(r)
        
        limiter.mutex.Lock()
        defer limiter.mutex.Unlock()
        
        now := time.Now()
        cutoff := now.Add(-limiter.window)
        
        // Clean old requests
        requests := limiter.requests[ip]
        validRequests := []time.Time{}
        for _, reqTime := range requests {
            if reqTime.After(cutoff) {
                validRequests = append(validRequests, reqTime)
            }
        }
        
        if len(validRequests) >= limiter.limit {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        
        validRequests = append(validRequests, now)
        limiter.requests[ip] = validRequests
        
        next.ServeHTTP(w, r)
    })
}

func getClientIP(r *http.Request) string {
    ip := r.Header.Get("X-Forwarded-For")
    if ip == "" {
        ip = r.Header.Get("X-Real-IP")
    }
    if ip == "" {
        ip = r.RemoteAddr
    }
    return ip
}

2. CORS Configuration

Configure CORS properly:

go
// middleware/cors.go
package middleware

import (
    "net/http"
)

func CORSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        
        // Whitelist allowed origins
        allowedOrigins := []string{
            "https://yourdomain.com",
            "https://app.yourdomain.com",
        }
        
        allowed := false
        for _, allowedOrigin := range allowedOrigins {
            if origin == allowedOrigin {
                allowed = true
                break
            }
        }
        
        if allowed {
            w.Header().Set("Access-Control-Allow-Origin", origin)
        }
        
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        w.Header().Set("Access-Control-Allow-Credentials", "true")
        w.Header().Set("Access-Control-Max-Age", "3600")
        
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

3. Secure Headers

Add security headers:

go
// middleware/security.go
package middleware

import "net/http"

func SecurityHeadersMiddleware(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", "1; mode=block")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        
        next.ServeHTTP(w, r)
    })
}

File Upload Security

1. Validate File Types and Sizes

go
// handlers/upload.go
package handlers

import (
    "io"
    "net/http"
    "path/filepath"
)

const (
    maxFileSize = 10 * 1024 * 1024 // 10MB
    allowedExts = ".jpg,.jpeg,.png,.pdf"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(maxFileSize)
    
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "Error retrieving file", http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // Validate file extension
    ext := filepath.Ext(header.Filename)
    if !isAllowedExtension(ext) {
        http.Error(w, "File type not allowed", http.StatusBadRequest)
        return
    }
    
    // Validate file size
    if header.Size > maxFileSize {
        http.Error(w, "File too large", http.StatusBadRequest)
        return
    }
    
    // Read file content
    fileBytes := make([]byte, header.Size)
    if _, err := file.Read(fileBytes); err != nil && err != io.EOF {
        http.Error(w, "Error reading file", http.StatusInternalServerError)
        return
    }
    
    // Validate file content (magic bytes)
    if !isValidFileContent(fileBytes, ext) {
        http.Error(w, "Invalid file content", http.StatusBadRequest)
        return
    }
    
    // Process file
    // ...
}

func isAllowedExtension(ext string) bool {
    allowed := []string{".jpg", ".jpeg", ".png", ".pdf"}
    for _, allowedExt := range allowed {
        if strings.ToLower(ext) == allowedExt {
            return true
        }
    }
    return false
}

func isValidFileContent(content []byte, ext string) bool {
    // Check magic bytes
    if ext == ".jpg" || ext == ".jpeg" {
        return len(content) >= 2 && content[0] == 0xFF && content[1] == 0xD8
    }
    if ext == ".png" {
        return len(content) >= 8 && 
               content[0] == 0x89 && content[1] == 0x50 && 
               content[2] == 0x4E && content[3] == 0x47
    }
    // Add more validations...
    return true
}

Environment Variables Security

1. Use Environment Variables for Secrets

Never hardcode secrets:

go
// ✅ Good: Use environment variables
package config

import "os"

type Config struct {
    DatabaseURL string
    JWTSecret   string
    APIKey      string
}

func LoadConfig() *Config {
    return &Config{
        DatabaseURL: os.Getenv("DATABASE_URL"),
        JWTSecret:   os.Getenv("JWT_SECRET"),
        APIKey:      os.Getenv("API_KEY"),
    }
}

// ❌ Bad: Hardcoded secrets
var jwtSecret = "my-secret-key" // Never do this!

2. Validate Required Environment Variables

go
// config/config.go
package config

import (
    "fmt"
    "os"
)

func LoadConfig() (*Config, error) {
    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        return nil, fmt.Errorf("DATABASE_URL is required")
    }
    
    jwtSecret := os.Getenv("JWT_SECRET")
    if jwtSecret == "" {
        return nil, fmt.Errorf("JWT_SECRET is required")
    }
    
    if len(jwtSecret) < 32 {
        return nil, fmt.Errorf("JWT_SECRET must be at least 32 characters")
    }
    
    return &Config{
        DatabaseURL: dbURL,
        JWTSecret:   jwtSecret,
    }, nil
}

Error Handling Security

1. Don't Expose Internal Errors

go
// ✅ Good: Generic error messages
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.URL.Query().Get("id")
    
    user, err := getUserByID(userID)
    if err != nil {
        // Log detailed error internally
        log.Printf("Error fetching user %s: %v", userID, err)
        
        // Return generic error to client
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    json.NewEncoder(w).Encode(user)
}

// ❌ Bad: Exposing internal errors
func getUserHandlerBad(w http.ResponseWriter, r *http.Request) {
    user, err := getUserByID(userID)
    if err != nil {
        // Exposes internal structure!
        http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
        return
    }
}

Best Practices Summary

  1. Validate all input using whitelist approach
  2. Use parameterized queries to prevent SQL injection
  3. Hash passwords with bcrypt or argon2
  4. Implement JWT tokens securely with proper expiration
  5. Add rate limiting to prevent abuse
  6. Configure CORS properly with whitelist
  7. Add security headers to all responses
  8. Validate file uploads (type, size, content)
  9. Use environment variables for secrets
  10. Don't expose internal errors to clients
  11. Keep dependencies updated for security patches
  12. Use HTTPS in production

Conclusion

Security in Go applications requires attention to input validation, SQL injection prevention, secure authentication, and proper API design. By following these practices, you can build robust, secure Go applications that protect both your users and your data. Remember that security is an ongoing process—regular security audits and dependency updates are essential.

References

Want more insights?

Subscribe to our newsletter or follow us for more updates on software development and team scaling.

Contact Us