Skip to main content
โšก Calmops

Secure Coding Practices in Go

Secure Coding Practices in Go

Introduction

Secure coding practices are fundamental to building trustworthy applications. This guide covers common vulnerabilities, prevention techniques, and best practices for writing secure Go code.

Core Principles

Defense in Depth

Use multiple layers of security:

  1. Input validation
  2. Authentication
  3. Authorization
  4. Encryption
  5. Logging and monitoring

Principle of Least Privilege

Grant minimum necessary permissions:

  • Run services with minimal privileges
  • Use least permissive file permissions
  • Limit database user permissions

Fail Securely

When errors occur, fail in a secure state:

  • Don’t expose sensitive information in errors
  • Log security events
  • Deny access by default

Good: Secure Error Handling

Safe Error Messages

package main

import (
	"fmt"
	"log"
)

// โœ… GOOD: Generic error messages to users
func AuthenticateUser(username, password string) error {
	// Don't reveal which field is wrong
	if !isValidCredentials(username, password) {
		return fmt.Errorf("invalid credentials")
	}
	return nil
}

// โœ… GOOD: Detailed logging for debugging
func AuthenticateUserWithLogging(username, password string) error {
	if username == "" {
		log.Printf("Authentication failed: empty username")
		return fmt.Errorf("invalid credentials")
	}
	
	if !isValidPassword(password) {
		log.Printf("Authentication failed for user %s: invalid password", username)
		return fmt.Errorf("invalid credentials")
	}
	
	return nil
}

// โŒ BAD: Revealing sensitive information
func BadAuthenticateUser(username, password string) error {
	if username == "" {
		return fmt.Errorf("username is empty")
	}
	
	if !isValidPassword(password) {
		return fmt.Errorf("password is incorrect for user %s", username)
	}
	
	return nil
}

func isValidCredentials(username, password string) bool {
	return username != "" && password != ""
}

func isValidPassword(password string) bool {
	return len(password) >= 8
}

Good: Secure Configuration

Environment-Based Configuration

package main

import (
	"fmt"
	"os"
)

// โœ… GOOD: Load sensitive config from environment
type Config struct {
	DatabaseURL string
	APIKey      string
	JWTSecret   string
}

func LoadConfig() (*Config, error) {
	config := &Config{
		DatabaseURL: os.Getenv("DATABASE_URL"),
		APIKey:      os.Getenv("API_KEY"),
		JWTSecret:   os.Getenv("JWT_SECRET"),
	}
	
	// Validate required fields
	if config.DatabaseURL == "" {
		return nil, fmt.Errorf("DATABASE_URL not set")
	}
	
	return config, nil
}

// โŒ BAD: Hardcoded secrets
type BadConfig struct {
	DatabaseURL string
	APIKey      string
	JWTSecret   string
}

func BadLoadConfig() *BadConfig {
	return &BadConfig{
		DatabaseURL: "postgres://user:password@localhost/db",
		APIKey:      "sk_live_abc123xyz",
		JWTSecret:   "my-secret-key",
	}
}

Good: Secure File Operations

Safe File Handling

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// โœ… GOOD: Prevent path traversal
func SafeReadFile(baseDir, filename string) ([]byte, error) {
	// Resolve to absolute path
	absPath, err := filepath.Abs(filepath.Join(baseDir, filename))
	if err != nil {
		return nil, err
	}
	
	// Ensure path is within base directory
	absBase, _ := filepath.Abs(baseDir)
	if !strings.HasPrefix(absPath, absBase) {
		return nil, fmt.Errorf("path traversal detected")
	}
	
	return os.ReadFile(absPath)
}

// โœ… GOOD: Secure file permissions
func CreateSecureFile(filename string) error {
	// Create with restrictive permissions (0600 = rw-------)
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		return err
	}
	defer file.Close()
	
	return nil
}

// โŒ BAD: Path traversal vulnerability
func BadReadFile(baseDir, filename string) ([]byte, error) {
	// No validation - allows ../../../etc/passwd
	return os.ReadFile(filepath.Join(baseDir, filename))
}

// โŒ BAD: Insecure file permissions
func BadCreateFile(filename string) error {
	// World-readable file (0666 = rw-rw-rw-)
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		return err
	}
	defer file.Close()
	
	return nil
}

Good: Secure Logging

Safe Logging Practices

package main

import (
	"log"
	"strings"
)

// โœ… GOOD: Sanitize sensitive data before logging
func LogUserAction(username, action string) {
	// Don't log passwords or tokens
	if action == "login" {
		log.Printf("User %s performed login", username)
	} else {
		log.Printf("User %s performed %s", username, action)
	}
}

// โœ… GOOD: Redact sensitive information
func RedactSensitiveData(data string) string {
	// Redact credit card numbers
	data = strings.ReplaceAll(data, "4532", "****")
	
	// Redact API keys
	if strings.Contains(data, "api_key=") {
		data = strings.ReplaceAll(data, "api_key=", "api_key=***")
	}
	
	return data
}

// โŒ BAD: Logging sensitive data
func BadLogUserAction(username, password string) {
	log.Printf("User %s logged in with password %s", username, password)
}

// โŒ BAD: Logging full request data
func BadLogRequest(requestData string) {
	log.Printf("Request: %s", requestData) // May contain sensitive data
}

Good: Secure Dependencies

Dependency Management

package main

import (
	"fmt"
	"os/exec"
)

// โœ… GOOD: Regularly update dependencies
func UpdateDependencies() error {
	cmd := exec.Command("go", "get", "-u", "./...")
	return cmd.Run()
}

// โœ… GOOD: Audit dependencies for vulnerabilities
func AuditDependencies() error {
	cmd := exec.Command("go", "list", "-json", "-m", "all")
	return cmd.Run()
}

// โœ… GOOD: Use go.mod for version pinning
// go.mod example:
// require (
//     github.com/some/package v1.2.3
// )

// โŒ BAD: Using unversioned dependencies
// import "github.com/some/package" // No version control

Best Practices

1. Validate All Input

// โœ… GOOD: Validate at entry point
func HandleRequest(input string) error {
	if err := ValidateInput(input); err != nil {
		return fmt.Errorf("invalid input: %w", err)
	}
	return processInput(input)
}

2. Use HTTPS

// โœ… GOOD: Use HTTPS in production
func StartServer() error {
	return http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
}

// โŒ BAD: Use HTTP
func BadStartServer() error {
	return http.ListenAndServe(":80", nil)
}

3. Implement Rate Limiting

// โœ… GOOD: Rate limit API endpoints
import "golang.org/x/time/rate"

limiter := rate.NewLimiter(rate.Limit(10), 1)

func HandleRequest(w http.ResponseWriter, r *http.Request) {
	if !limiter.Allow() {
		http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
		return
	}
	// Handle request
}

4. Use Security Headers

// โœ… GOOD: Set security headers
func SetSecurityHeaders(w http.ResponseWriter) {
	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")
}

Resources

Summary

Secure coding requires vigilance and following best practices. Always validate input, handle errors securely, protect sensitive data, and keep dependencies updated. Remember: security is not a feature, it’s a requirement.

Comments