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
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:
// 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, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
return s
}2. Use Whitelist Validation
Prefer whitelist over blacklist validation:
// ✅ 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:
// ✅ 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:
// ✅ 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
// 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:
// ✅ 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
// 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
// ✅ 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
- Validate all input using whitelist approach
- Use parameterized queries to prevent SQL injection
- Hash passwords with bcrypt or argon2
- Implement JWT tokens securely with proper expiration
- Add rate limiting to prevent abuse
- Configure CORS properly with whitelist
- Add security headers to all responses
- Validate file uploads (type, size, content)
- Use environment variables for secrets
- Don't expose internal errors to clients
- Keep dependencies updated for security patches
- 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