This repository contains examples and implementations of secure coding practices for Go applications, following the OWASP Go Secure Coding Practices Guide.
- Introduction
- Input Validation
- Output Encoding
- Authentication and Password Management
- Session Management
- Access Control
- Cryptographic Practices
- Error Handling and Logging
- Data Protection
- Communication Security
- System Configuration
- Database Security
- File Management
- Memory Management
- General Coding Practices
- Resources
This guide implements secure coding practices for Go web applications based on the OWASP Secure Coding Practices Quick Reference Guide. The goal is to help developers avoid common security vulnerabilities while building robust Go applications.
- Go developers building web applications
- Security-conscious programmers
- Developers transitioning from other languages to Go
- Anyone looking to improve their secure coding skills
Location: input-validation/
Input validation is the first line of defense against many security vulnerabilities including SQL injection, XSS, command injection, and path traversal attacks.
- Validate all input - Never trust user input
- Use allow-lists over deny-lists - Define what is acceptable rather than what is not
- Validate on the server-side - Client-side validation can be bypassed
- Canonicalize before validation - Handle URL encoding, Unicode normalization, etc.
func SanitizeInput(input string) string {
// Handle URL encoding
input, _ = url.QueryUnescape(input)
// Remove null bytes
input = strings.ReplaceAll(input, "\x00", "")
// Ensure valid UTF-8
if !utf8.ValidString(input) {
input = strings.ToValidUTF8(input, "")
}
// Remove control characters
input = strings.Map(func(r rune) rune {
if r < 0x20 && r != '\n' && r != '\r' && r != '\t' {
return -1
}
return r
}, input)
return input
}var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-.]{2,31}$`)
func ValidateUsername(username string) error {
username = SanitizeInput(username)
if !usernameRegex.MatchString(username) {
return errors.New("invalid username format")
}
return nil
}func ValidateEmail(email string) error {
email = SanitizeInput(email)
addr, err := mail.ParseAddress(email)
if err != nil {
return errors.New("invalid email format")
}
// Additional validation
if len(addr.Address) > 254 {
return errors.New("email too long")
}
return nil
}func ValidateAge(age int) error {
if age < 0 || age > 150 {
return errors.New("age must be between 0 and 150")
}
return nil
}- ✅ Validate data type, length, format, and range
- ✅ Use strong typing where possible
- ✅ Validate file uploads (type, size, content)
- ✅ Sanitize input before validation
- ✅ Use parameterized queries for database operations
- ❌ Don't rely solely on client-side validation
- ❌ Don't use blacklists for validation
Proper output encoding prevents XSS and injection attacks by ensuring data is treated as data, not executable code.
import "html/template"
func SafeHTMLOutput(userInput string) string {
return template.HTMLEscapeString(userInput)
}func SafeJSOutput(userInput string) string {
return template.JSEscapeString(userInput)
}import "net/url"
func SafeURLParameter(param string) string {
return url.QueryEscape(param)
}- ✅ Use context-aware encoding (HTML, JavaScript, URL, CSS)
- ✅ Use Go's
html/templatepackage for HTML rendering - ✅ Encode all untrusted data before output
- ✅ Use appropriate content-type headers
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
// Use appropriate cost factor (10-12 for bcrypt)
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword(
[]byte(hashedPassword),
[]byte(password),
)
}- ✅ Use bcrypt, scrypt, or Argon2 for password hashing
- ✅ Implement strong password policies
- ✅ Use multi-factor authentication where possible
- ✅ Implement account lockout after failed attempts
- ✅ Use secure password reset mechanisms
- ❌ Never store passwords in plain text
- ❌ Don't use weak hashing algorithms (MD5, SHA1)
import "github.com/gorilla/sessions"
func NewSecureStore(secretKey []byte) *sessions.CookieStore {
store := sessions.NewCookieStore(secretKey)
store.Options = &sessions.Options{
Path: "/",
MaxAge: 3600, // 1 hour
HttpOnly: true,
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode,
}
return store
}- ✅ Generate strong session IDs (cryptographically random)
- ✅ Set appropriate session timeouts
- ✅ Use HttpOnly and Secure flags on cookies
- ✅ Regenerate session ID after login
- ✅ Implement proper session invalidation on logout
- ✅ Use SameSite cookie attribute
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}- ✅ Deny by default
- ✅ Implement least privilege principle
- ✅ Check authorization on every request
- ✅ Use role-based access control (RBAC)
- ✅ Protect sensitive resources
- ❌ Don't rely on client-side access control
import "crypto/rand"
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}func GenerateRandomString(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}
for i := range b {
b[i] = charset[int(b[i])%len(charset)]
}
return string(b), nil
}- ✅ Use
crypto/randfor random number generation - ✅ Use established cryptographic libraries
- ✅ Keep cryptographic keys secure
- ✅ Use TLS 1.2+ for transport security
- ❌ Don't implement your own cryptographic algorithms
- ❌ Don't use
math/randfor security purposes
func HandleError(w http.ResponseWriter, err error, userMsg string) {
// Log detailed error server-side
log.Printf("Error: %v", err)
// Return generic message to user
http.Error(w, userMsg, http.StatusInternalServerError)
}import "log/slog"
func LogSecurityEvent(event string, details map[string]any) {
slog.Info("security_event",
"event", event,
"details", details,
"timestamp", time.Now(),
)
}- ✅ Log all authentication attempts
- ✅ Log access control failures
- ✅ Log input validation failures
- ✅ Use structured logging
- ✅ Protect log files from unauthorized access
- ❌ Don't log sensitive data (passwords, tokens, PII)
- ❌ Don't expose stack traces to users
import "crypto/aes"
import "crypto/cipher"
func Encrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}- ✅ Encrypt sensitive data at rest
- ✅ Use strong encryption algorithms (AES-256)
- ✅ Protect encryption keys
- ✅ Use secure key management systems
- ✅ Implement secure data deletion
func StartSecureServer(addr string, handler http.Handler) error {
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
srv := &http.Server{
Addr: addr,
Handler: handler,
TLSConfig: cfg,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
return srv.ListenAndServeTLS("cert.pem", "key.pem")
}- ✅ Use TLS 1.2 or higher
- ✅ Use strong cipher suites
- ✅ Implement HTTP Strict Transport Security (HSTS)
- ✅ Validate SSL/TLS certificates
- ✅ Use secure HTTP headers
func GetUserByEmail(db *sql.DB, email string) (*User, error) {
var user User
query := "SELECT id, username, email FROM users WHERE email = ?"
err := db.QueryRow(query, email).Scan(&user.ID, &user.Username, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}- ✅ Always use parameterized queries
- ✅ Use least privilege database accounts
- ✅ Encrypt database connections
- ✅ Validate input before database operations
- ❌ Never concatenate user input into SQL queries
func ValidateFileUpload(file multipart.File, header *multipart.FileHeader) error {
// Check file size
if header.Size > 10*1024*1024 { // 10MB
return errors.New("file too large")
}
// Check file extension
allowedExtensions := map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".pdf": true,
}
ext := strings.ToLower(filepath.Ext(header.Filename))
if !allowedExtensions[ext] {
return errors.New("file type not allowed")
}
// Verify file content matches extension
buffer := make([]byte, 512)
_, err := file.Read(buffer)
if err != nil {
return err
}
contentType := http.DetectContentType(buffer)
file.Seek(0, 0) // Reset file pointer
// Validate content type
// ... additional validation
return nil
}- ✅ Validate file type, size, and content
- ✅ Use allow-lists for file extensions
- ✅ Store files outside web root
- ✅ Generate random filenames
- ✅ Scan uploaded files for malware
- ❌ Don't trust file extensions alone
// Use defer for cleanup
func ProcessData() error {
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close()
// Process file...
return nil
}- ✅ Use
deferfor resource cleanup - ✅ Close database connections properly
- ✅ Use context for cancellation and timeouts
- ✅ Be careful with goroutine leaks
- ✅ Use sync.Pool for frequently allocated objects
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("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}import "golang.org/x/time/rate"
func RateLimitMiddleware(limiter *rate.Limiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}- ✅ Keep dependencies up to date
- ✅ Use static analysis tools (gosec, staticcheck)
- ✅ Implement proper error handling
- ✅ Use security linters
- ✅ Follow Go idioms and best practices
- ✅ Conduct security code reviews
- ✅ Implement defense in depth
- gosec - Go security checker
- govulncheck - Vulnerability scanner
- staticcheck - Static analysis tool
golang.org/x/crypto- Cryptographic packagesgithub.com/gorilla/sessions- Session managementgithub.com/gorilla/csrf- CSRF protectiongolang.org/x/time/rate- Rate limiting
This repository is organized as a comprehensive secure coding learning platform:
secure-coding/
├── README.md # Main documentation
├── go.work # Optional: workspace for multiple go.mod files
├── .github/
│ └── workflows/
│ ├── ci.yml # Continuous integration pipeline
│ └── publish.yml # Publishing workflow
├── tools/ # Scripts, installers, dev tools
│ ├── bin/
│ └── make.sh
├── hack/ # Helper scripts for dev (db init, seed data)
├── docs/ # General guides, rubrics, slides
├── infra/ # Infrastructure as code
│ ├── docker/ # Docker compose files
│ └── k8s/ # Kubernetes manifests, Terraform
├── pkg/ # Reusable libraries (can be consumed across levels)
│ ├── logger/ # Structured logging utilities
│ ├── auth/ # Authentication helpers
│ └── crypto/ # Cryptographic utilities
├── internal/ # Internal shared code (not importable externally)
│ └── testutil/ # Testing utilities
├── exercises/ # Hands-on practice exercises
│ ├── level-1-input-output/
│ │ ├── README.md # Exercise description and objectives
│ │ ├── starter/ # Starter code for students
│ │ │ ├── go.mod
│ │ │ └── cmd/...
│ │ ├── solution/ # Reference solution (private branch or restricted)
│ │ ├── tests/ # Unit and integration tests
│ │ └── exercise.yaml # Metadata: objectives, steps, hints
│ ├── level-2-auth/
│ │ └── ...
│ ├── level-3-rbac/
│ │ └── ...
│ ├── level-4-crypto/
│ │ └── ...
│ └── level-5-database/
│ └── ...
├── examples/ # Small runnable examples and reference implementations
├── playground/ # Ephemeral experiments and templates for students
└── security/ # Security policies, threat models, checklist.md
exercises/: Progressive learning modules organized by difficulty levelpkg/: Shared, reusable packages that demonstrate secure coding patternsinternal/: Internal utilities not meant for external consumptionexamples/: Quick reference implementations for common security patternsplayground/: Sandbox environment for experimentationinfra/: Infrastructure setup for deploying secure applicationssecurity/: Security documentation, policies, and checklists
Contributions are welcome! Please ensure:
- Code follows Go best practices
- Security principles are properly implemented
- Examples are well-documented
- Tests are included
This project follows the OWASP Go-SCP guide, which is released under the Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0).
These examples are for educational purposes. Always conduct thorough security testing and consider your specific use case when implementing security controls.
Remember: Security is not a one-time task but a continuous process. Stay updated with the latest security advisories and best practices.