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:
- Input validation
- Authentication
- Authorization
- Encryption
- 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
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- Go Security: https://golang.org/doc/security
- CWE/SANS Top 25: https://cwe.mitre.org/top25/
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