From 306b5e8845d5c4d39309b4ed5ebd55af186f1698 Mon Sep 17 00:00:00 2001 From: Mark Curphey Date: Fri, 23 Jan 2026 06:12:49 +0000 Subject: [PATCH] docs: comprehensive security guide modernization for Go 1.24+ Add 13 new security sections covering modern Go security practices: - HTTP Server Security (Slowloris protection, timeouts) - Rate Limiting (token bucket, per-client limiting) - Path Traversal Prevention (Go 1.24 os.Root, Zip Slip) - Command Injection Prevention (exec.Command, allowlists) - CORS Security (rs/cors configuration) - Context Timeouts (database, HTTP, goroutine management) - Secrets Management (Vault, AWS Secrets Manager, Kubernetes) - Container Security (scratch images, Kubernetes hardening) - Content Security Policy (nonces, strict-dynamic) - Passkeys/WebAuthn (FIDO2 passwordless authentication) - Security Tooling (govulncheck, gosec, fuzzing) - Claude Code Security Development (OWASP Go agent, hooks) - Security Scanning Integration (CI/CD, pre-commit) Enhance existing sections with modern patterns: - Input Validation: go-playground/validator examples - SQL Injection: complete CRUD, IN clause, ORDER BY allowlist - Cryptographic Practices: Go 1.22 ChaCha8Rand, Go 1.24 FIPS 140-3 - Password Management: Argon2id, NIST 800-63B guidelines - Session Management: JWT v5 migration, SameSite cookies - Error Handling: Go 1.13+ error wrapping with errors.Is/As - Logging: log/slog structured logging - CSRF: Gorilla alternatives after archive - Memory Management: race detection section Also adds CLAUDE.md for Claude Code guidance with project overview, build commands, structure, and git flow documentation. Updates align with OWASP Top 10 2021 and current Go best practices. Closes #95 (Gorilla toolkit status) Addresses #71 (code samples) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 53 +++ src/SUMMARY.md | 16 +- src/access-control/URL.go | 55 ++- src/access-control/rate-limiting.md | 396 ++++++++++++++++ .../passkeys.md | 222 +++++++++ .../password-policies.md | 111 ++++- .../validation-and-storage.md | 104 +++- src/communication-security/cors-security.md | 338 +++++++++++++ .../http-server-security.md | 323 +++++++++++++ src/communication-security/http-tls.md | 22 +- .../pseudo-random-generators.md | 129 ++++- src/data-protection/secrets-management.md | 428 +++++++++++++++++ src/development-tools/README.md | 11 + src/development-tools/claude-code-security.md | 410 ++++++++++++++++ src/development-tools/security-scanning.md | 443 ++++++++++++++++++ .../context-timeouts.md | 392 ++++++++++++++++ src/error-handling-logging/error-handling.md | 73 +++ src/error-handling-logging/logging.md | 62 ++- src/file-management/path-traversal.md | 377 +++++++++++++++ src/general-coding-practices/README.md | 73 ++- .../cross-site-request-forgery.md | 45 +- src/input-validation/README.md | 6 +- src/input-validation/command-injection.md | 339 ++++++++++++++ src/input-validation/validation.md | 180 ++++++- src/memory-management/README.md | 88 ++++ src/output-encoding/README.md | 4 +- .../content-security-policy.md | 276 +++++++++++ src/output-encoding/cross-site-scripting.md | 21 +- src/output-encoding/sql-injection.md | 259 +++++++++- src/security-tooling/README.md | 333 +++++++++++++ src/session-management/README.md | 45 +- src/session-management/session.go | 43 +- .../container-security.md | 409 ++++++++++++++++ 33 files changed, 5968 insertions(+), 118 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/access-control/rate-limiting.md create mode 100644 src/authentication-password-management/passkeys.md create mode 100644 src/communication-security/cors-security.md create mode 100644 src/communication-security/http-server-security.md create mode 100644 src/data-protection/secrets-management.md create mode 100644 src/development-tools/README.md create mode 100644 src/development-tools/claude-code-security.md create mode 100644 src/development-tools/security-scanning.md create mode 100644 src/error-handling-logging/context-timeouts.md create mode 100644 src/file-management/path-traversal.md create mode 100644 src/input-validation/command-injection.md create mode 100644 src/output-encoding/content-security-policy.md create mode 100644 src/security-tooling/README.md create mode 100644 src/system-configuration/container-security.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..19f3637 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the OWASP Go Secure Coding Practices Guide - a GitBook-based documentation project providing security guidance for Go web application developers. The book follows the OWASP Secure Coding Practices Quick Reference Guide. + +## Build Commands + +```bash +# Install dependencies and build all formats (PDF, ePub, Mobi, DOCX) +npm i && node_modules/.bin/gitbook install && npm run build + +# Live preview with hot reload +npm run serve + +# Build individual formats +npm run build:pdf +npm run build:epub +npm run build:mobi +npm run build:docx + +# Build using Docker (alternative) +docker-compose run -u node:node --rm build +``` + +## Project Structure + +- `src/` - All book content in Markdown format +- `src/SUMMARY.md` - Book table of contents (defines chapter order) +- `src/README.md` - Book introduction +- `dist/` - Generated output files (PDF, ePub, Mobi) +- `book.json` - GitBook configuration +- `.go` files in `src/` subdirectories - Example code snippets referenced by chapters + +## Content Organization + +Book chapters follow OWASP security categories: +- Input Validation, Output Encoding +- Authentication/Password Management, Session Management +- Access Control, Cryptographic Practices +- Error Handling/Logging, Data Protection +- Communication Security, System Configuration +- Database Security, File Management, Memory Management +- General Coding Practices (CSRF, Regex) + +## Git Flow + +This project uses git-flow branching model: +- Features: `git flow feature start ` → PR to `develop` +- Hotfixes: `git flow hotfix start` → applies to both `develop` and `master` +- Releases are handled by project owner from `develop` to `master` diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9b39c09..dd3e18b 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -5,35 +5,49 @@ Summary * [Input Validation](input-validation/README.md) * [Validation](input-validation/validation.md) * [Sanitization](input-validation/sanitization.md) + * [Command Injection](input-validation/command-injection.md) * [Output Encoding](output-encoding/README.md) * [XSS - Cross-Site Scripting](output-encoding/cross-site-scripting.md) * [SQL Injection](output-encoding/sql-injection.md) + * [Content Security Policy](output-encoding/content-security-policy.md) * [Authentication and Password Management](authentication-password-management/README.md) * [Communicating authentication data](authentication-password-management/communicating-authentication-data.md) * [Validation and Storage](authentication-password-management/validation-and-storage.md) * [Password policies](authentication-password-management/password-policies.md) + * [Passkeys and WebAuthn](authentication-password-management/passkeys.md) * [Other guidelines](authentication-password-management/other-guidelines.md) * [Session Management](session-management/README.md) * [Access Control](access-control/README.md) + * [Rate Limiting](access-control/rate-limiting.md) * [Cryptographic Practices](cryptographic-practices/README.md) * [Pseudo-Random Generators](cryptographic-practices/pseudo-random-generators.md) * [Error Handling and Logging](error-handling-logging/README.md) * [Error Handling](error-handling-logging/error-handling.md) * [Logging](error-handling-logging/logging.md) + * [Context Timeouts](error-handling-logging/context-timeouts.md) * [Data Protection](data-protection/README.md) + * [Secrets Management](data-protection/secrets-management.md) * [Communication Security](communication-security/README.md) * [HTTP/TLS](communication-security/http-tls.md) + * [HTTP Server Security](communication-security/http-server-security.md) + * [CORS Security](communication-security/cors-security.md) * [WebSockets](communication-security/websockets.md) * [System Configuration](system-configuration/README.md) + * [Container Security](system-configuration/container-security.md) * [Database Security](database-security/README.md) * [Connections](database-security/connections.md) * [Authentication](database-security/authentication.md) * [Parameterized Queries](database-security/parameterized-queries.md) * [Stored Procedures](database-security/stored-procedures.md) * [File Management](file-management/README.md) + * [Path Traversal](file-management/path-traversal.md) * [Memory Management](memory-management/README.md) -* General Coding Practices +* [General Coding Practices](general-coding-practices/README.md) * [Cross-Site Request Forgery](general-coding-practices/cross-site-request-forgery.md) * [Regular Expressions](general-coding-practices/regular-expressions.md) +* [Security Tooling](security-tooling/README.md) +* [Development Tools](development-tools/README.md) + * [Claude Code Security Development](development-tools/claude-code-security.md) + * [Security Scanning Integration](development-tools/security-scanning.md) * [How To Contribute](howto-contribute.md) * [Final Notes](final-notes.md) diff --git a/src/access-control/URL.go b/src/access-control/URL.go index 2b96924..26511be 100644 --- a/src/access-control/URL.go +++ b/src/access-control/URL.go @@ -5,9 +5,10 @@ import ( "fmt" "log" "net/http" + "os" "time" - "github.com/dgrijalva/jwt-go" //JWT is not in the native Go packages + "github.com/golang-jwt/jwt/v5" // Maintained JWT library (replaces deprecated dgrijalva/jwt-go) ) type Key int @@ -17,7 +18,17 @@ const MyKey Key = 0 // JWT schema of the data it will store type Claims struct { Username string `json:"username"` - jwt.StandardClaims + jwt.RegisteredClaims +} + +// getJWTSecret retrieves the JWT secret from environment variables. +// SECURITY: Never hardcode secrets in source code. +func getJWTSecret() []byte { + secret := os.Getenv("JWT_SECRET") + if secret == "" { + log.Fatal("JWT_SECRET environment variable is required") + } + return []byte(secret) } func Homepage(res http.ResponseWriter, req *http.Request) { @@ -30,28 +41,38 @@ func Homepage(res http.ResponseWriter, req *http.Request) { // create a JWT and put in the clients cookie func setToken(res http.ResponseWriter, req *http.Request) { - //30m Expiration for non-sensitive applications - OWSAP - expireToken := time.Now().Add(time.Minute * 30).Unix() + // 30m Expiration for non-sensitive applications - OWASP expireCookie := time.Now().Add(time.Minute * 30) - //token Claims + // token Claims using jwt/v5 RegisteredClaims claims := Claims{ - "TestUser", - jwt.StandardClaims{ - ExpiresAt: expireToken, + Username: "TestUser", + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expireCookie), Issuer: "localhost:9000", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, _ := token.SignedString([]byte("secret")) - - //Set Cookie parameters - //Expires - 30m - //HTTP only - //Path - //Domain - cookie := http.Cookie{Name: "Auth", Value: signedToken, Expires: expireCookie, HttpOnly: true, Path: "/", Domain: "127.0.0.1", Secure: true} + signedToken, err := token.SignedString(getJWTSecret()) + if err != nil { + http.Error(res, "Internal server error", http.StatusInternalServerError) + log.Printf("Error signing token: %v", err) + return + } + + // Set Cookie parameters + // SameSite attribute helps prevent CSRF attacks + cookie := http.Cookie{ + Name: "Auth", + Value: signedToken, + Expires: expireCookie, // 30 min + HttpOnly: true, // Prevents JavaScript access + Path: "/", + Domain: "127.0.0.1", + Secure: true, // Only sent over HTTPS + SameSite: http.SameSiteStrictMode, // CSRF protection + } http.SetCookie(res, &cookie) http.Redirect(res, req, "/profile", http.StatusTemporaryRedirect) } @@ -71,7 +92,7 @@ func validate(page http.HandlerFunc) http.HandlerFunc { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } - return []byte("secret"), nil + return getJWTSecret(), nil }) if err != nil { res.Header().Set("Content-Type", "text/html") diff --git a/src/access-control/rate-limiting.md b/src/access-control/rate-limiting.md new file mode 100644 index 0000000..09ab01b --- /dev/null +++ b/src/access-control/rate-limiting.md @@ -0,0 +1,396 @@ +Rate Limiting +============= + +Rate limiting is essential for preventing abuse, ensuring service stability, +and protecting against denial-of-service attacks. This section covers +implementing rate limiting in Go applications. + +Why Rate Limiting Matters +------------------------- + +Without rate limiting, your application is vulnerable to: +- **Brute force attacks**: Unlimited login attempts +- **API abuse**: Single clients consuming all resources +- **Denial of service**: Overwhelming your server with requests +- **Scraping**: Automated extraction of your data + +Token Bucket Algorithm +---------------------- + +Go's `golang.org/x/time/rate` package implements the token bucket algorithm, +which allows bursts of traffic while maintaining an average rate limit. + +```bash +go get golang.org/x/time/rate +``` + +Basic Rate Limiter +------------------ + +### Global Rate Limiting + +Limit all requests to your server: + +```go +package main + +import ( + "net/http" + "golang.org/x/time/rate" +) + +// Allow 100 requests per second with burst of 200 +var limiter = rate.NewLimiter(rate.Limit(100), 200) + +func rateLimitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !limiter.Allow() { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", handler) + + // Apply rate limiting + http.ListenAndServe(":8080", rateLimitMiddleware(mux)) +} +``` + +Per-Client Rate Limiting +------------------------ + +Global rate limiting has a flaw: one abusive client can exhaust the limit for +everyone. Per-client limiting solves this: + +```go +package main + +import ( + "net/http" + "sync" + "time" + + "golang.org/x/time/rate" +) + +// Client stores rate limiter for each visitor +type Client struct { + limiter *rate.Limiter + lastSeen time.Time +} + +var ( + clients = make(map[string]*Client) + mu sync.RWMutex +) + +// getClient returns the rate limiter for a client IP +func getClient(ip string) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + client, exists := clients[ip] + if !exists { + // 10 requests per second, burst of 30 + limiter := rate.NewLimiter(rate.Limit(10), 30) + clients[ip] = &Client{limiter: limiter, lastSeen: time.Now()} + return limiter + } + + client.lastSeen = time.Now() + return client.limiter +} + +// cleanupClients removes old entries to prevent memory leaks +func cleanupClients() { + for { + time.Sleep(time.Minute) + mu.Lock() + for ip, client := range clients { + if time.Since(client.lastSeen) > 3*time.Minute { + delete(clients, ip) + } + } + mu.Unlock() + } +} + +func perClientRateLimiter(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get client IP (handle proxies - see security note below) + ip := r.RemoteAddr + + limiter := getClient(ip) + if !limiter.Allow() { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} + +func main() { + // Start cleanup goroutine + go cleanupClients() + + mux := http.NewServeMux() + mux.HandleFunc("/", handler) + + http.ListenAndServe(":8080", perClientRateLimiter(mux)) +} +``` + +Secure Client IP Detection +-------------------------- + +**Security Warning**: Extracting client IPs is tricky behind proxies. Naive +implementations are vulnerable to spoofing. + +```go +// INSECURE - Attacker can spoof X-Forwarded-For +func getIPInsecure(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + return strings.Split(xff, ",")[0] // Easily spoofed! + } + return r.RemoteAddr +} + +// SECURE - Only trust X-Forwarded-For from known proxy +func getIPSecure(r *http.Request, trustedProxies map[string]bool) string { + remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr) + + // Only trust headers if request came from known proxy + if trustedProxies[remoteIP] { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + ips := strings.Split(xff, ",") + // Get rightmost non-proxy IP + for i := len(ips) - 1; i >= 0; i-- { + ip := strings.TrimSpace(ips[i]) + if !trustedProxies[ip] { + return ip + } + } + } + } + + return remoteIP +} +``` + +Endpoint-Specific Limits +------------------------ + +Different endpoints may need different limits: + +```go +type RateLimitConfig struct { + Rate rate.Limit + Burst int +} + +var endpointLimits = map[string]RateLimitConfig{ + "/api/login": {Rate: 5, Burst: 10}, // Strict for auth + "/api/search": {Rate: 30, Burst: 50}, // Moderate + "/api/public": {Rate: 100, Burst: 200}, // Generous +} + +func getEndpointLimiter(path string, ip string) *rate.Limiter { + config, exists := endpointLimits[path] + if !exists { + config = RateLimitConfig{Rate: 50, Burst: 100} // Default + } + + key := path + ":" + ip + // ... get or create limiter for this key +} +``` + +Using Third-Party Libraries +--------------------------- + +### Tollbooth + +Full-featured rate limiting middleware: + +```bash +go get github.com/didip/tollbooth/v7 +``` + +```go +package main + +import ( + "net/http" + "time" + + "github.com/didip/tollbooth/v7" + "github.com/didip/tollbooth/v7/limiter" +) + +func main() { + // Create a limiter: 1 request per second per IP + lmt := tollbooth.NewLimiter(1, &limiter.ExpirableOptions{ + DefaultExpirationTTL: time.Hour, + }) + + // Configure what identifies a client + lmt.SetIPLookup(limiter.IPLookup{ + Name: "X-Real-IP", + IndexFromRight: 0, + }) + + // Set custom message + lmt.SetMessage("You have exceeded the rate limit") + lmt.SetMessageContentType("text/plain; charset=utf-8") + + mux := http.NewServeMux() + mux.Handle("/", tollbooth.LimitHandler(lmt, http.HandlerFunc(handler))) + + http.ListenAndServe(":8080", mux) +} +``` + +### httprate (Chi middleware) + +For chi router users: + +```bash +go get github.com/go-chi/httprate +``` + +```go +package main + +import ( + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/httprate" +) + +func main() { + r := chi.NewRouter() + + // Global rate limit + r.Use(httprate.LimitByIP(100, time.Minute)) + + // Stricter limit for auth endpoints + r.Group(func(r chi.Router) { + r.Use(httprate.LimitByIP(10, time.Minute)) + r.Post("/login", loginHandler) + r.Post("/register", registerHandler) + }) + + http.ListenAndServe(":8080", r) +} +``` + +Distributed Rate Limiting +------------------------- + +For multi-server deployments, use Redis for shared state: + +```go +package main + +import ( + "context" + "net/http" + "time" + + "github.com/go-redis/redis/v8" +) + +var rdb *redis.Client + +func init() { + rdb = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) +} + +func rateLimitRedis(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + ip := r.RemoteAddr + key := "ratelimit:" + ip + + // Increment counter + count, err := rdb.Incr(ctx, key).Result() + if err != nil { + // Fail open or closed based on your requirements + next.ServeHTTP(w, r) + return + } + + // Set expiry on first request + if count == 1 { + rdb.Expire(ctx, key, time.Minute) + } + + // Check limit (100 requests per minute) + if count > 100 { + w.Header().Set("Retry-After", "60") + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +Response Headers +---------------- + +Communicate rate limit status to clients: + +```go +func rateLimitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + limiter := getClientLimiter(r) + + // Set rate limit headers + w.Header().Set("X-RateLimit-Limit", "100") + w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", int(limiter.Tokens()))) + w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) + + if !limiter.Allow() { + w.Header().Set("Retry-After", "60") + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +Best Practices +-------------- + +1. **Start conservative**: Begin with stricter limits; loosen based on data +2. **Monitor and alert**: Log rate limit hits to detect attacks +3. **Differentiate by endpoint**: Auth endpoints need stricter limits +4. **Handle burst traffic**: Allow reasonable bursts for legitimate use +5. **Fail safely**: Decide whether to fail open or closed on errors +6. **Use appropriate identifiers**: IP for anonymous, user ID for authenticated +7. **Clean up state**: Prevent memory leaks with periodic cleanup + +References +---------- + +- [golang.org/x/time/rate Documentation][1] +- [Tollbooth Rate Limiter][2] +- [OWASP Blocking Brute Force Attacks][3] + +[1]: https://pkg.go.dev/golang.org/x/time/rate +[2]: https://github.com/didip/tollbooth +[3]: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks diff --git a/src/authentication-password-management/passkeys.md b/src/authentication-password-management/passkeys.md new file mode 100644 index 0000000..9262470 --- /dev/null +++ b/src/authentication-password-management/passkeys.md @@ -0,0 +1,222 @@ +Passkeys and WebAuthn +===================== + +Passkeys (based on the WebAuthn/FIDO2 standard) represent the future of +authentication. They replace passwords with cryptographic key pairs, providing +phishing-resistant authentication without the usability issues of passwords. + +Why Passkeys? +------------- + +| Feature | Passwords | Passkeys | +|---------|-----------|----------| +| Phishing resistance | No | Yes | +| Credential reuse | Common | Impossible | +| Server breaches | Expose hashes | No secrets stored | +| User friction | High | Low (biometric) | +| Brute force | Possible | Impossible | + +How Passkeys Work +----------------- + +1. **Registration**: User's device generates a public/private key pair. The + public key is sent to the server; the private key never leaves the device. + +2. **Authentication**: Server sends a challenge. The device signs it with the + private key (after biometric/PIN verification). Server verifies the signature + with the stored public key. + +Implementation in Go +-------------------- + +The [go-webauthn/webauthn][1] library provides WebAuthn support for Go +applications. + +### Installation + +```bash +go get github.com/go-webauthn/webauthn/webauthn +``` + +### Basic Setup + +```go +package main + +import ( + "github.com/go-webauthn/webauthn/webauthn" +) + +var webAuthn *webauthn.WebAuthn + +func init() { + var err error + webAuthn, err = webauthn.New(&webauthn.Config{ + RPDisplayName: "Your Application", // Display name + RPID: "example.com", // Your domain + RPOrigins: []string{"https://example.com"}, // Allowed origins + }) + if err != nil { + panic(err) + } +} + +// User must implement webauthn.User interface +type User struct { + ID []byte + Name string + DisplayName string + Credentials []webauthn.Credential +} + +func (u *User) WebAuthnID() []byte { return u.ID } +func (u *User) WebAuthnName() string { return u.Name } +func (u *User) WebAuthnDisplayName() string { return u.DisplayName } +func (u *User) WebAuthnCredentials() []webauthn.Credential { return u.Credentials } +``` + +### Registration Flow + +```go +// Step 1: Begin registration (send to client) +func beginRegistration(w http.ResponseWriter, r *http.Request) { + user := getCurrentUser(r) // Get user from session + + options, session, err := webAuthn.BeginRegistration(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Store session data (use secure session storage) + storeWebAuthnSession(r, session) + + // Send options to client + json.NewEncoder(w).Encode(options) +} + +// Step 2: Complete registration (receive from client) +func finishRegistration(w http.ResponseWriter, r *http.Request) { + user := getCurrentUser(r) + session := getWebAuthnSession(r) + + credential, err := webAuthn.FinishRegistration(user, *session, r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Store credential for user in database + user.Credentials = append(user.Credentials, *credential) + saveUser(user) + + w.WriteHeader(http.StatusOK) +} +``` + +### Authentication Flow + +```go +// Step 1: Begin authentication (send to client) +func beginLogin(w http.ResponseWriter, r *http.Request) { + user := getUserByUsername(r.URL.Query().Get("username")) + + options, session, err := webAuthn.BeginLogin(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + storeWebAuthnSession(r, session) + json.NewEncoder(w).Encode(options) +} + +// Step 2: Complete authentication (receive from client) +func finishLogin(w http.ResponseWriter, r *http.Request) { + user := getUserByUsername(r.URL.Query().Get("username")) + session := getWebAuthnSession(r) + + credential, err := webAuthn.FinishLogin(user, *session, r) + if err != nil { + http.Error(w, "Authentication failed", http.StatusUnauthorized) + return + } + + // Update credential sign count (prevents cloning attacks) + updateCredentialSignCount(user, credential) + + // Create authenticated session + createUserSession(w, r, user) + + w.WriteHeader(http.StatusOK) +} +``` + +### Client-Side JavaScript + +```javascript +// Registration +async function registerPasskey() { + // Get options from server + const optionsResponse = await fetch('/webauthn/register/begin'); + const options = await optionsResponse.json(); + + // Create credential (triggers biometric prompt) + const credential = await navigator.credentials.create({ + publicKey: options.publicKey + }); + + // Send credential to server + await fetch('/webauthn/register/finish', { + method: 'POST', + body: JSON.stringify(credential), + headers: { 'Content-Type': 'application/json' } + }); +} + +// Authentication +async function loginWithPasskey(username) { + const optionsResponse = await fetch(`/webauthn/login/begin?username=${username}`); + const options = await optionsResponse.json(); + + const assertion = await navigator.credentials.get({ + publicKey: options.publicKey + }); + + await fetch('/webauthn/login/finish', { + method: 'POST', + body: JSON.stringify(assertion), + headers: { 'Content-Type': 'application/json' } + }); +} +``` + +Security Considerations +----------------------- + +1. **Store credentials securely**: Public keys and credential IDs should be + stored securely in your database. + +2. **Verify sign count**: Track and verify the signature counter to detect + cloned authenticators. + +3. **Use attestation carefully**: Attestation can verify authenticator + properties but has privacy implications. + +4. **Fallback authentication**: Consider backup authentication methods for + users who lose their authenticators. + +5. **Multiple credentials**: Allow users to register multiple passkeys + (phone, laptop, security key). + +Migration Strategy +------------------ + +For existing applications: + +1. **Phase 1**: Add passkey support as optional 2FA alongside passwords +2. **Phase 2**: Encourage passkey registration at login +3. **Phase 3**: Allow passwordless login for users with passkeys +4. **Phase 4**: (Optional) Make passkeys the primary authentication method + +[1]: https://github.com/go-webauthn/webauthn diff --git a/src/authentication-password-management/password-policies.md b/src/authentication-password-management/password-policies.md index f91a0ab..c99cbcf 100644 --- a/src/authentication-password-management/password-policies.md +++ b/src/authentication-password-management/password-policies.md @@ -10,17 +10,69 @@ Because passwords are not easy to manage and remember. Users not only tend to use weak passwords (e.g. "123456") they can easily remember, they can also re-use the same password for different services. -If your application sign-in requires a password, the best you can do is to -"_enforce password complexity requirements, (...) requiring the use of -alphabetic as well as numeric and/or special characters)_". Password length -should also be enforced: "_eight characters is commonly used, but 16 is -better or consider the use of multi-word pass phrases_". - -Of course, none of the previous guidelines will prevent users from re-using -the same password. The best you can do to reduce this bad practice is to -"_enforce password changes_", and preventing password re-use. "_Critical systems -may require more frequent changes. The time between resets must be -administratively controlled_". +Modern Password Guidelines (NIST 800-63B) +----------------------------------------- + +Password guidance has evolved significantly. [NIST Special Publication 800-63B][1] +provides updated recommendations that differ from historical practices: + +**Length over complexity:** +- Require a minimum of 8 characters (NIST minimum) +- Allow at least 64 characters maximum +- Consider multi-word passphrases (e.g., "correct-horse-battery-staple") +- **Do NOT** require arbitrary complexity rules (uppercase, symbols, etc.) as + they lead to predictable patterns + +**Password checks:** +- Check passwords against known breach databases (e.g., [Have I Been Pwned][2]) +- Block common passwords (e.g., "password", "123456", company name) +- Block context-specific passwords (username, email, service name) + +**What NOT to do (contrary to legacy advice):** +- **Do NOT** require periodic password rotation - This leads to weaker passwords + as users make predictable changes (Password1 → Password2) +- **Do NOT** use password hints or knowledge-based authentication (security + questions) - These are vulnerable to social engineering and data breaches +- **Do NOT** require SMS-based 2FA for sensitive applications - Use + authenticator apps or hardware tokens instead + +**Encourage MFA:** +The best way to protect accounts is Multi-Factor Authentication. Consider +supporting: +- TOTP authenticator apps (Google Authenticator, Authy) +- Hardware security keys (WebAuthn/FIDO2) +- Passkeys (see [Passkeys section](passkeys.md)) + +Implementation Example +---------------------- + +```go +// Check password against common passwords and breach databases +func validatePassword(password, username, email string) error { + // Minimum length + if len(password) < 8 { + return errors.New("password must be at least 8 characters") + } + + // Check against context-specific strings + lowerPass := strings.ToLower(password) + if strings.Contains(lowerPass, strings.ToLower(username)) { + return errors.New("password cannot contain username") + } + + // Check against common passwords (maintain a blocklist) + if isCommonPassword(password) { + return errors.New("password is too common") + } + + // Optionally check against breach databases + if isBreachedPassword(password) { + return errors.New("password has appeared in a data breach") + } + + return nil +} +``` ## Reset @@ -30,15 +82,32 @@ Such a mechanism is as critical as signup or sign-in, and you're encouraged to follow the best practices to be sure your system does not disclose sensitive data and become compromised. -"_Passwords should be at least one day old before they can be changed_". This -way you'll prevent attacks on password re-use. Whenever using "_email based -resets, only send email to a pre-registered address with a temporary -link/password_" which should have a short expiration period. +**Secure Password Reset Flow:** + +1. **Use email-based resets only** - Send a reset link (not a temporary password) + to a pre-registered email address +2. **Use cryptographically secure tokens** - Generate random tokens using + `crypto/rand`, not predictable values +3. **Set short expiration** - Reset links should expire within 1 hour maximum +4. **Single use** - Invalidate the token after use or after a new one is generated +5. **Notify the user** - Send an email notification when a password is changed +6. **Rate limit requests** - Prevent enumeration and abuse + +```go +// Generate a secure reset token +func generateResetToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} +``` -Whenever a password reset is requested, the user should be notified. -The same way, temporary passwords should be changed on the next usage. +**Avoid these legacy practices:** +- **Security questions** - Answers are often guessable or found on social media +- **Sending passwords via email** - Always use one-time reset links instead +- **SMS-based reset** - Vulnerable to SIM swapping attacks -A common practice for password reset is the "Security Question", whose answer -was previously configured by the account owner. "_Password reset questions -should support sufficiently random answers_": asking for "Favorite Book?" may -lead to "The Bible" which makes this reset questions undesirable in most cases. +[1]: https://pages.nist.gov/800-63-3/sp800-63b.html +[2]: https://haveibeenpwned.com/API/v3 diff --git a/src/authentication-password-management/validation-and-storage.md b/src/authentication-password-management/validation-and-storage.md index bb22898..2d0d52e 100644 --- a/src/authentication-password-management/validation-and-storage.md +++ b/src/authentication-password-management/validation-and-storage.md @@ -128,19 +128,99 @@ standards reviewed and approved by experts. It is therefore important to use them instead of trying to re-invent the wheel. In the case of password storage, the hashing algorithms recommended by -[OWASP][2] are [`bcrypt`][2], [`PDKDF2`][3], [`Argon2`][4] and [`scrypt`][5]. -Those take care of hashing and salting passwords in a robust way. Go authors -provide an extended package for cryptography, that is not part of the standard -library. It provides robust implementations for most of the aforementioned -algorithms. It can be downloaded using `go get`: +[OWASP][2] are (in order of preference): **Argon2id**, bcrypt, scrypt, and +PBKDF2. Those take care of hashing and salting passwords in a robust way. Go +authors provide an extended package for cryptography, that is not part of the +standard library. It provides robust implementations for most of the +aforementioned algorithms. It can be downloaded using `go get`: ``` go get golang.org/x/crypto ``` -The following example shows how to use bcrypt, which should be good enough for -most of the situations. The advantage of bcrypt is that it is simpler to use, -and is therefore less error-prone. +### Argon2id (Recommended) + +Argon2id is the winner of the Password Hashing Competition and is OWASP's +first recommendation. It provides excellent resistance against both GPU-based +and side-channel attacks. + +```go +package main + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +// Argon2id parameters - OWASP recommended minimum values +const ( + argonTime = 1 // Number of iterations + argonMemory = 64 * 1024 // Memory in KiB (64 MB) + argonThreads = 4 // Number of threads + argonKeyLen = 32 // Length of the derived key + argonSaltLen = 16 // Length of the salt +) + +func hashPassword(password string) (string, error) { + // Generate a random salt + salt := make([]byte, argonSaltLen) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + // Hash the password with Argon2id + hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) + + // Encode for storage: $argon2id$v=19$m=65536,t=1,p=4$$ + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + + return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, argonMemory, argonTime, argonThreads, b64Salt, b64Hash), nil +} + +func verifyPassword(password, encodedHash string) (bool, error) { + // Parse the encoded hash + parts := strings.Split(encodedHash, "$") + if len(parts) != 6 { + return false, fmt.Errorf("invalid hash format") + } + + var version int + var memory, time uint32 + var threads uint8 + _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads) + if err != nil { + return false, err + } + _, _ = fmt.Sscanf(parts[2], "v=%d", &version) + + salt, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + return false, err + } + expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) + if err != nil { + return false, err + } + + // Compute hash with same parameters + computedHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(expectedHash))) + + // Use constant-time comparison to prevent timing attacks + return subtle.ConstantTimeCompare(expectedHash, computedHash) == 1, nil +} +``` + +### bcrypt (Good Alternative) + +The following example shows how to use bcrypt, which is simpler to use and is +therefore less error-prone. It's a good choice when you need simplicity. ```go package main @@ -225,8 +305,8 @@ remind to check for known issues. [^1]: Hashing functions are the subject of Collisions but recommended hashing functions have a really low collisions probability [1]: ../cryptographic-practices/pseudo-random-generators.md -[2]: https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet -[3]: https://godoc.org/golang.org/x/crypto/bcrypt -[4]: https://github.com/p-h-c/phc-winner-argon2 -[5]: https://godoc.org/golang.org/x/crypto/pbkdf2 +[2]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html +[3]: https://pkg.go.dev/golang.org/x/crypto/bcrypt +[4]: https://pkg.go.dev/golang.org/x/crypto/argon2 +[5]: https://pkg.go.dev/golang.org/x/crypto/pbkdf2 [6]: https://github.com/ermites-io/passwd diff --git a/src/communication-security/cors-security.md b/src/communication-security/cors-security.md new file mode 100644 index 0000000..18a64c9 --- /dev/null +++ b/src/communication-security/cors-security.md @@ -0,0 +1,338 @@ +CORS Security +============= + +Cross-Origin Resource Sharing (CORS) controls which domains can access your +API. Misconfigured CORS can expose your application to cross-site attacks. + +Understanding CORS +------------------ + +Browsers enforce the Same-Origin Policy, blocking requests from different +origins. CORS headers tell browsers which cross-origin requests to allow. + +**Origin** = Protocol + Domain + Port +- `https://example.com` and `https://api.example.com` are different origins +- `http://example.com` and `https://example.com` are different origins + +The Security Risk +----------------- + +```go +// INSECURE - Allows any origin with credentials +func handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Credentials", "true") // DANGEROUS! +} +``` + +This allows any website to make authenticated requests to your API, enabling: +- **CSRF attacks**: Malicious sites can perform actions as logged-in users +- **Data theft**: Attackers can read sensitive user data +- **Session hijacking**: Credentials can be stolen + +**Note**: Browsers block `Allow-Credentials: true` with `Allow-Origin: *`, but +some implementations have bugs or workarounds. + +Secure CORS Configuration +------------------------- + +### Manual Implementation + +```go +func corsMiddleware(allowedOrigins map[string]bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + + // Only allow specific origins + if allowedOrigins[origin] { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Credentials", "true") + 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-Max-Age", "86400") // 24 hours + } + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func main() { + allowedOrigins := map[string]bool{ + "https://example.com": true, + "https://app.example.com": true, + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/", apiHandler) + + http.ListenAndServe(":8080", corsMiddleware(allowedOrigins)(mux)) +} +``` + +Using rs/cors Library +--------------------- + +The `rs/cors` library is the most popular CORS middleware for Go: + +```bash +go get github.com/rs/cors +``` + +### Basic Secure Configuration + +```go +package main + +import ( + "net/http" + "github.com/rs/cors" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/api/", apiHandler) + + // Secure CORS configuration + c := cors.New(cors.Options{ + AllowedOrigins: []string{"https://example.com", "https://app.example.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"}, + ExposedHeaders: []string{"X-Request-ID"}, + AllowCredentials: true, + MaxAge: 86400, // Preflight cache for 24 hours + }) + + http.ListenAndServe(":8080", c.Handler(mux)) +} +``` + +### Dynamic Origin Validation + +For complex origin requirements, use `AllowOriginFunc`: + +```go +import ( + "net/url" + "strings" +) + +c := cors.New(cors.Options{ + AllowOriginFunc: func(origin string) bool { + // Parse the origin + u, err := url.Parse(origin) + if err != nil { + return false + } + + // Only allow HTTPS + if u.Scheme != "https" { + return false + } + + // Allow subdomains of example.com + if u.Host == "example.com" || strings.HasSuffix(u.Host, ".example.com") { + return true + } + + return false + }, + AllowCredentials: true, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, +}) +``` + +**Security Warning**: Be very careful with `AllowOriginFunc`. Overly permissive +validation can lead to security vulnerabilities. + +Framework-Specific Examples +--------------------------- + +### Gin + +```go +import "github.com/gin-contrib/cors" + +func main() { + r := gin.Default() + + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"https://example.com"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowHeaders: []string{"Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + r.GET("/api/data", handler) + r.Run(":8080") +} +``` + +### Chi + +```go +import "github.com/go-chi/cors" + +func main() { + r := chi.NewRouter() + + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, + })) + + r.Get("/api/data", handler) + http.ListenAndServe(":8080", r) +} +``` + +### Fiber + +```go +import "github.com/gofiber/fiber/v2/middleware/cors" + +func main() { + app := fiber.New() + + app.Use(cors.New(cors.Config{ + AllowOrigins: "https://example.com,https://app.example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Content-Type,Authorization", + AllowCredentials: true, + MaxAge: 86400, + })) + + app.Get("/api/data", handler) + app.Listen(":8080") +} +``` + +Common Mistakes +--------------- + +### Mistake 1: Reflecting Origin Without Validation + +```go +// INSECURE - Reflects any origin +func handler(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + w.Header().Set("Access-Control-Allow-Origin", origin) // Any origin! + w.Header().Set("Access-Control-Allow-Credentials", "true") +} +``` + +### Mistake 2: Overly Permissive Subdomain Matching + +```go +// INSECURE - Attacker can register evil.example.com.attacker.com +func isAllowed(origin string) bool { + return strings.Contains(origin, "example.com") // Too loose! +} + +// SECURE - Proper suffix check +func isAllowed(origin string) bool { + u, _ := url.Parse(origin) + return u.Host == "example.com" || strings.HasSuffix(u.Host, ".example.com") +} +``` + +### Mistake 3: Allowing null Origin + +```go +// INSECURE - 'null' origin comes from sandboxed iframes and file:// URLs +allowedOrigins := []string{"https://example.com", "null"} // Dangerous! +``` + +### Mistake 4: Missing Vary Header + +```go +// When origin-specific responses are cached, include Vary header +w.Header().Set("Vary", "Origin") +w.Header().Set("Access-Control-Allow-Origin", origin) +``` + +Development vs Production +------------------------- + +Use different configurations for development: + +```go +func getCORSConfig() cors.Options { + if os.Getenv("ENV") == "development" { + return cors.Options{ + AllowedOrigins: []string{"http://localhost:3000", "http://localhost:5173"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + AllowCredentials: true, + } + } + + // Production - strict configuration + return cors.Options{ + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + AllowCredentials: true, + MaxAge: 86400, + } +} +``` + +Testing CORS Configuration +-------------------------- + +```bash +# Test preflight request +curl -X OPTIONS https://api.example.com/data \ + -H "Origin: https://example.com" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type" \ + -v + +# Test actual request +curl https://api.example.com/data \ + -H "Origin: https://example.com" \ + -v + +# Should see: +# Access-Control-Allow-Origin: https://example.com +# Access-Control-Allow-Credentials: true +``` + +Best Practices Summary +---------------------- + +| Practice | Recommendation | +|----------|----------------| +| Origins | Explicit allowlist, never `*` with credentials | +| Methods | Only methods your API actually uses | +| Headers | Only headers your API actually needs | +| Credentials | Only enable if cookies/auth headers needed | +| Max-Age | Cache preflight (86400 = 24 hours) | +| Validation | Use exact matching or careful suffix checks | +| Vary Header | Include when responses vary by origin | + +References +---------- + +- [MDN CORS Documentation][1] +- [OWASP CORS Cheat Sheet][2] +- [rs/cors Library][3] + +[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +[2]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Origin_Resource_Sharing_Cheat_Sheet.html +[3]: https://github.com/rs/cors diff --git a/src/communication-security/http-server-security.md b/src/communication-security/http-server-security.md new file mode 100644 index 0000000..b5ce1a6 --- /dev/null +++ b/src/communication-security/http-server-security.md @@ -0,0 +1,323 @@ +HTTP Server Security +==================== + +Securing your Go HTTP server is critical for production deployments. This +section covers timeout configuration, denial-of-service protection, and +secure server setup. + +The Default Server Problem +-------------------------- + +Go's convenience functions for starting HTTP servers are **unsafe for +production** because they have no timeout configuration: + +```go +// INSECURE - Never use in production! +http.ListenAndServe(":8080", handler) // No timeouts +http.ListenAndServeTLS(":443", cert, key, h) // No timeouts +http.Serve(listener, handler) // No timeouts +``` + +Without timeouts, your server is vulnerable to: +- **Slowloris attacks**: Attackers open connections and send data very slowly +- **Resource exhaustion**: Connections accumulate until "too many open files" +- **Hanging connections**: Misbehaving clients can hold resources indefinitely + +Secure Server Configuration +--------------------------- + +Always create a custom `http.Server` with explicit timeouts: + +```go +package main + +import ( + "log" + "net/http" + "time" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", handler) + + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, // Slowloris protection + ReadTimeout: 30 * time.Second, // Max time to read request + WriteTimeout: 30 * time.Second, // Max time to write response + IdleTimeout: 120 * time.Second, // Keep-alive timeout + MaxHeaderBytes: 1 << 20, // 1 MB max header size + } + + log.Printf("Server starting on %s", srv.Addr) + log.Fatal(srv.ListenAndServe()) +} +``` + +Understanding Timeouts +---------------------- + +Each timeout serves a specific security purpose: + +| Timeout | Purpose | Recommended | +|---------|---------|-------------| +| `ReadHeaderTimeout` | Time to read request headers (Slowloris protection) | 10-30s | +| `ReadTimeout` | Time to read entire request including body | 30-60s | +| `WriteTimeout` | Time to write response (from end of header read) | 30-60s | +| `IdleTimeout` | Time between requests on keep-alive connections | 60-120s | + +### Timeout Flow Diagram + +``` +Connection ReadHeaderTimeout ReadTimeout WriteTimeout +Accepted ──────────┬─────────────────┬──────────────────────┬────── + │ │ │ + Headers Read Body Read Response Written +``` + +Slowloris Attack Protection +--------------------------- + +The Slowloris attack works by: +1. Opening many connections to your server +2. Sending partial HTTP requests very slowly +3. Never completing requests, keeping connections open +4. Eventually exhausting server resources + +**Protection requires `ReadHeaderTimeout`:** + +```go +// VULNERABLE - No ReadHeaderTimeout +srv := &http.Server{ + ReadTimeout: 30 * time.Second, // Not enough! +} + +// SECURE - ReadHeaderTimeout set +srv := &http.Server{ + ReadHeaderTimeout: 10 * time.Second, // Closes slow header sends + ReadTimeout: 30 * time.Second, +} +``` + +Handler-Level Timeouts +---------------------- + +For fine-grained control, use `http.TimeoutHandler` to wrap specific handlers: + +```go +// Wrap handler with 30-second timeout +handler := http.TimeoutHandler(myHandler, 30*time.Second, "Request timeout") + +mux.Handle("/api/slow-endpoint", handler) +``` + +This returns HTTP 503 Service Unavailable if the handler exceeds the timeout. + +### Per-Request Context Timeouts + +For more control, use context timeouts within handlers: + +```go +func handler(w http.ResponseWriter, r *http.Request) { + // Create timeout context for this request + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + // Use ctx for database calls, external APIs, etc. + result, err := db.QueryContext(ctx, "SELECT ...") + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + http.Error(w, "Request timeout", http.StatusGatewayTimeout) + return + } + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + // ... +} +``` + +Streaming Response Considerations +--------------------------------- + +`WriteTimeout` is problematic for streaming responses (SSE, WebSockets, large +file downloads) because it's an absolute deadline, not an idle timeout. + +**Options for streaming:** + +1. **Disable WriteTimeout for streaming endpoints** (use separate server): +```go +// Streaming server without WriteTimeout +streamSrv := &http.Server{ + Addr: ":8081", + Handler: streamMux, + ReadHeaderTimeout: 10 * time.Second, + // WriteTimeout intentionally omitted for streaming +} +``` + +2. **Use http.Hijacker** for manual connection control: +```go +func streamHandler(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Streaming not supported", http.StatusInternalServerError) + return + } + conn, _, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + + // Manual connection management with per-write deadlines + for data := range dataChannel { + conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) + conn.Write(data) + } +} +``` + +Connection Limits +----------------- + +Limit concurrent connections to prevent resource exhaustion: + +```go +import "golang.org/x/net/netutil" + +func main() { + listener, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatal(err) + } + + // Limit to 1000 concurrent connections + listener = netutil.LimitListener(listener, 1000) + + srv := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + // ... other timeouts + } + + log.Fatal(srv.Serve(listener)) +} +``` + +Graceful Shutdown +----------------- + +Implement graceful shutdown to complete in-flight requests: + +```go +func main() { + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + // Start server in goroutine + go func() { + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("Server error: %v", err) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + + // Give outstanding requests 30 seconds to complete + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server stopped") +} +``` + +Complete Secure Server Example +------------------------------ + +```go +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", homeHandler) + mux.HandleFunc("/api/", apiHandler) + + // Wrap with security middleware + handler := securityHeaders(mux) + + srv := &http.Server{ + Addr: ":8080", + Handler: handler, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 20, // 1 MB + } + + // Graceful shutdown + go func() { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + srv.Shutdown(ctx) + }() + + log.Printf("Server starting on %s", srv.Addr) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("Server error: %v", err) + } +} + +func securityHeaders(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("Content-Security-Policy", "default-src 'self'") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + next.ServeHTTP(w, r) + }) +} +``` + +References +---------- + +- [Cloudflare Go Timeout Guide][1] +- [Go net/http Server Documentation][2] +- [OWASP Denial of Service Cheat Sheet][3] + +[1]: https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ +[2]: https://pkg.go.dev/net/http#Server +[3]: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html diff --git a/src/communication-security/http-tls.md b/src/communication-security/http-tls.md index b6e1d69..27afb7c 100644 --- a/src/communication-security/http-tls.md +++ b/src/communication-security/http-tls.md @@ -111,17 +111,33 @@ version can be set with the following configurations: ```go // MinVersion contains the minimum SSL/TLS version that is acceptable. -// If zero, then TLS 1.0 is taken as the minimum. - MinVersion uint16 +// If zero, then TLS 1.2 is taken as the minimum (as of Go 1.18+). +// IMPORTANT: TLS 1.0 and 1.1 are deprecated and should not be used. +MinVersion uint16 ``` ```go // MaxVersion contains the maximum SSL/TLS version that is acceptable. // If zero, then the maximum version supported by this package is used, -// which is currently TLS 1.2. +// which is currently TLS 1.3 (supported since Go 1.12). MaxVersion uint16 ``` +**Modern TLS Configuration (recommended):** + +```go +config := &tls.Config{ + MinVersion: tls.VersionTLS12, // TLS 1.0/1.1 are deprecated + // TLS 1.3 is used automatically when available (default since Go 1.13) + // No need to set MaxVersion unless you have specific requirements +} +``` + +TLS 1.3 offers improved security and performance over TLS 1.2, including: +- Faster handshakes (1-RTT, or 0-RTT for resumed sessions) +- Removal of legacy cryptographic algorithms +- Encrypted handshake messages + The safety of the used ciphers can be checked with [SSL Labs][4]. An additional flag that is commonly used to mitigate downgrade attacks is the diff --git a/src/cryptographic-practices/pseudo-random-generators.md b/src/cryptographic-practices/pseudo-random-generators.md index 165bd71..067b4de 100644 --- a/src/cryptographic-practices/pseudo-random-generators.md +++ b/src/cryptographic-practices/pseudo-random-generators.md @@ -90,7 +90,128 @@ the first example. Yes, you guessed it, you would be able to predict the user's password! -[1]: https://golang.org/pkg/math/rand/ -[2]: https://golang.org/pkg/math/rand/#pkg-overview -[3]: https://golang.org/pkg/math/rand/#Seed -[4]: https://golang.org/pkg/crypto/rand/ +Go 1.22: ChaCha8Rand Improvements +--------------------------------- + +Go 1.22 introduced a significant security improvement: the `math/rand` package +now uses **ChaCha8Rand**, a cryptographically-strong pseudorandom number +generator based on the ChaCha stream cipher. + +### What Changed + +| Aspect | Pre-Go 1.22 | Go 1.22+ | +|--------|-------------|----------| +| Algorithm | Linear feedback shift register | ChaCha8 stream cipher | +| Seed size | 63 bits | 256 bits | +| Predictability | ~607 values to predict all future outputs | Cryptographically secure | +| Default behavior | Deterministic with same seed | Random seed from crypto/rand | + +### Security Implications + +**Before Go 1.22:** +Using `math/rand` for security-sensitive operations was a "serious security +catastrophe" - an attacker observing output could predict all future values. + +**Go 1.22+:** +Accidental use of `math/rand` instead of `crypto/rand` is now "just a mistake" +rather than a critical vulnerability. However, `crypto/rand` is **still +recommended** for security-sensitive operations. + +### Recommendations + +```go +// ALWAYS use for security-sensitive operations +import "crypto/rand" + +func generateToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// OK for non-security uses (shuffling, sampling, etc.) +import "math/rand/v2" + +func shuffleItems(items []Item) { + rand.Shuffle(len(items), func(i, j int) { + items[i], items[j] = items[j], items[i] + }) +} +``` + +### math/rand vs math/rand/v2 + +Go 1.22 introduced `math/rand/v2`: + +```go +// Old package (still works, uses ChaCha8 by default now) +import "math/rand" + +// New package (recommended for new code) +import "math/rand/v2" +``` + +The v2 package has cleaner APIs and always uses ChaCha8Rand. + +FIPS 140-3 Compliance (Go 1.24+) +-------------------------------- + +For applications in regulated environments (government, finance, healthcare), +Go 1.24 introduced native FIPS 140-3 support. + +### Enabling FIPS Mode + +```bash +# Build with FIPS-validated cryptography +GOFIPS140=latest go build ./... + +# Or use specific validated version +GOFIPS140=v1.0.0 go build ./... +``` + +### Runtime Verification + +```go +import "crypto/fips140" + +func main() { + if fips140.Enabled() { + log.Println("Running in FIPS 140-3 compliant mode") + } +} +``` + +### What FIPS Mode Changes + +- Uses FIPS-validated implementations of cryptographic algorithms +- Restricts to approved algorithms (AES, SHA-2, ECDSA, RSA, etc.) +- Disables non-approved algorithms +- No cgo required - pure Go implementation + +### When to Use FIPS Mode + +- Government contracts requiring FIPS compliance +- Financial services with regulatory requirements +- Healthcare applications handling PHI +- Any system requiring CMVP validation + +Summary: When to Use Each Package +--------------------------------- + +| Use Case | Package | +|----------|---------| +| Tokens, keys, secrets, passwords | `crypto/rand` | +| Session IDs, CSRF tokens | `crypto/rand` | +| Nonces, IVs for encryption | `crypto/rand` | +| Shuffling, sampling, simulations | `math/rand/v2` | +| Random delays, jitter | `math/rand/v2` | +| Test data generation | `math/rand/v2` | + +[1]: https://pkg.go.dev/math/rand +[2]: https://pkg.go.dev/math/rand#pkg-overview +[3]: https://pkg.go.dev/math/rand#Seed +[4]: https://pkg.go.dev/crypto/rand +[5]: https://go.dev/blog/chacha8rand +[6]: https://go.dev/doc/security/fips140 diff --git a/src/data-protection/secrets-management.md b/src/data-protection/secrets-management.md new file mode 100644 index 0000000..4586986 --- /dev/null +++ b/src/data-protection/secrets-management.md @@ -0,0 +1,428 @@ +Secrets Management +================== + +Proper secrets management is critical for application security. This section +covers how to handle API keys, database credentials, encryption keys, and +other sensitive configuration in Go applications. + +The Golden Rule +--------------- + +**Never hardcode secrets in source code.** + +```go +// NEVER DO THIS +const apiKey = "sk-live-abc123def456" +const dbPassword = "supersecret123" +``` + +Hardcoded secrets: +- Get committed to version control +- Appear in build artifacts and logs +- Cannot be rotated without code changes +- May be visible to anyone with code access + +Environment Variables (Basic) +----------------------------- + +The minimum acceptable approach for secrets: + +```go +package main + +import ( + "log" + "os" +) + +func main() { + // Read from environment + apiKey := os.Getenv("API_KEY") + if apiKey == "" { + log.Fatal("API_KEY environment variable required") + } + + dbPassword := os.Getenv("DB_PASSWORD") + if dbPassword == "" { + log.Fatal("DB_PASSWORD environment variable required") + } + + // Use the secrets... +} +``` + +### Required vs Optional Secrets + +```go +// MustGetEnv panics if the environment variable is not set +func MustGetEnv(key string) string { + value := os.Getenv(key) + if value == "" { + log.Fatalf("Required environment variable %s is not set", key) + } + return value +} + +// GetEnvOrDefault returns a default value if not set +func GetEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func main() { + // Required - will fail if not set + apiKey := MustGetEnv("API_KEY") + + // Optional with safe default + logLevel := GetEnvOrDefault("LOG_LEVEL", "info") +} +``` + +Configuration Files with Viper +------------------------------ + +For complex configuration, use [Viper](https://github.com/spf13/viper): + +```bash +go get github.com/spf13/viper +``` + +```go +package main + +import ( + "log" + "github.com/spf13/viper" +) + +type Config struct { + Database struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` // From env var + Name string `mapstructure:"name"` + } `mapstructure:"database"` + + API struct { + Key string `mapstructure:"key"` // From env var + Secret string `mapstructure:"secret"` // From env var + } `mapstructure:"api"` +} + +func LoadConfig() (*Config, error) { + // Set config file + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("/etc/myapp/") + + // Allow environment variables to override + viper.AutomaticEnv() + + // Map environment variables to config keys + viper.BindEnv("database.password", "DB_PASSWORD") + viper.BindEnv("api.key", "API_KEY") + viper.BindEnv("api.secret", "API_SECRET") + + // Set safe defaults (not real secrets!) + viper.SetDefault("database.host", "localhost") + viper.SetDefault("database.port", 5432) + + // Read config file + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, err + } + // Config file not found - use env vars only + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, err + } + + // Validate required secrets + if config.Database.Password == "" { + log.Fatal("DB_PASSWORD environment variable required") + } + if config.API.Key == "" { + log.Fatal("API_KEY environment variable required") + } + + return &config, nil +} +``` + +Config file (`config.yaml`) - **no secrets**: + +```yaml +database: + host: localhost + port: 5432 + user: myapp + name: myapp_production + # password: loaded from DB_PASSWORD env var + +api: + # key: loaded from API_KEY env var + # secret: loaded from API_SECRET env var +``` + +Secret Management Services +-------------------------- + +For production, use dedicated secret management: + +### HashiCorp Vault + +```bash +go get github.com/hashicorp/vault/api +``` + +```go +package main + +import ( + "context" + "log" + + vault "github.com/hashicorp/vault/api" +) + +func getSecretFromVault(path, key string) (string, error) { + config := vault.DefaultConfig() + client, err := vault.NewClient(config) + if err != nil { + return "", err + } + + // Token from environment or other auth method + // In production, use AppRole, Kubernetes auth, etc. + + secret, err := client.KVv2("secret").Get(context.Background(), path) + if err != nil { + return "", err + } + + value, ok := secret.Data[key].(string) + if !ok { + return "", fmt.Errorf("key %s not found", key) + } + + return value, nil +} + +func main() { + dbPassword, err := getSecretFromVault("myapp/database", "password") + if err != nil { + log.Fatalf("Failed to get database password: %v", err) + } + + // Use dbPassword... +} +``` + +### AWS Secrets Manager + +```bash +go get github.com/aws/aws-sdk-go-v2/service/secretsmanager +``` + +```go +package main + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +func getAWSSecret(secretName string) (map[string]string, error) { + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + client := secretsmanager.NewFromConfig(cfg) + + result, err := client.GetSecretValue(context.Background(), + &secretsmanager.GetSecretValueInput{ + SecretId: &secretName, + }) + if err != nil { + return nil, err + } + + var secrets map[string]string + if err := json.Unmarshal([]byte(*result.SecretString), &secrets); err != nil { + return nil, err + } + + return secrets, nil +} +``` + +### Kubernetes Secrets + +In Kubernetes, mount secrets as files or environment variables: + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: myapp + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: myapp-secrets + key: db-password + volumeMounts: + - name: secrets + mountPath: /etc/secrets + readOnly: true + volumes: + - name: secrets + secret: + secretName: myapp-secrets +``` + +Secure Logging +-------------- + +Never log secrets: + +```go +// INSECURE - Logging secrets +log.Printf("Connecting to database with password: %s", password) // NEVER! + +// SECURE - Log without secrets +log.Printf("Connecting to database at %s:%d as %s", host, port, user) + +// SECURE - Redact sensitive fields +type Config struct { + Host string + Password string +} + +func (c Config) String() string { + return fmt.Sprintf("Config{Host: %s, Password: [REDACTED]}", c.Host) +} +``` + +### Using slog with Redaction + +```go +import "log/slog" + +type RedactedString string + +func (r RedactedString) LogValue() slog.Value { + return slog.StringValue("[REDACTED]") +} + +type DatabaseConfig struct { + Host string + Port int + User string + Password RedactedString // Will show [REDACTED] in logs +} + +func main() { + config := DatabaseConfig{ + Host: "localhost", + Port: 5432, + User: "admin", + Password: "secret123", + } + + // Safe to log - password is redacted + slog.Info("database config loaded", slog.Any("config", config)) + // Output: database config loaded config={Host:localhost Port:5432 User:admin Password:[REDACTED]} +} +``` + +Secret Rotation +--------------- + +Design for secret rotation from the start: + +```go +type SecretProvider interface { + GetSecret(ctx context.Context, key string) (string, error) +} + +type DatabaseConnection struct { + secretProvider SecretProvider + secretKey string + currentSecret string + mu sync.RWMutex +} + +func (dc *DatabaseConnection) refreshSecret(ctx context.Context) error { + newSecret, err := dc.secretProvider.GetSecret(ctx, dc.secretKey) + if err != nil { + return err + } + + dc.mu.Lock() + dc.currentSecret = newSecret + dc.mu.Unlock() + + // Reconnect with new credentials + return dc.reconnect() +} + +// Run periodic refresh +func (dc *DatabaseConnection) StartSecretRefresh(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + go func() { + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + if err := dc.refreshSecret(ctx); err != nil { + slog.Error("failed to refresh secret", "error", err) + } + } + } + }() +} +``` + +Best Practices Summary +---------------------- + +| Do | Don't | +|----|-------| +| Use environment variables or secret managers | Hardcode secrets in code | +| Validate required secrets at startup | Fail silently if secrets missing | +| Log secret names, not values | Log secret values or connection strings | +| Implement secret rotation | Assume secrets never need changing | +| Use separate secrets per environment | Share secrets across environments | +| Encrypt secrets at rest | Store plaintext secrets in config files | +| Audit secret access | Allow unrestricted secret access | + +References +---------- + +- [12-Factor App: Config][1] +- [HashiCorp Vault][2] +- [AWS Secrets Manager][3] +- [Kubernetes Secrets][4] + +[1]: https://12factor.net/config +[2]: https://www.vaultproject.io/ +[3]: https://aws.amazon.com/secrets-manager/ +[4]: https://kubernetes.io/docs/concepts/configuration/secret/ diff --git a/src/development-tools/README.md b/src/development-tools/README.md new file mode 100644 index 0000000..1c15aab --- /dev/null +++ b/src/development-tools/README.md @@ -0,0 +1,11 @@ +Development Tools +================= + +This section covers security-focused development tools and workflows for Go +applications. Modern development increasingly involves AI assistants, automated +security scanning, and CI/CD integration. + +- [Claude Code Security Development](claude-code-security.md) - Using AI + assistants securely for Go development +- [Security Scanning Integration](security-scanning.md) - Integrating security + tools into your workflow diff --git a/src/development-tools/claude-code-security.md b/src/development-tools/claude-code-security.md new file mode 100644 index 0000000..3041dab --- /dev/null +++ b/src/development-tools/claude-code-security.md @@ -0,0 +1,410 @@ +Claude Code Security Development +================================ + +This section provides guidance for developers using Claude Code (or similar AI +assistants) to write secure Go applications. It includes project configuration, +security-focused prompts, and automated security review workflows. + +CLAUDE.md Security Template +--------------------------- + +Create a `CLAUDE.md` file in your Go project root to provide security context +to Claude Code. This file is automatically read and influences code generation. + +```markdown +# CLAUDE.md + +## Project Security Requirements + +This Go project follows OWASP Secure Coding Practices. All code must: + +### Authentication & Authorization +- Use Argon2id for password hashing (see auth/password.go) +- Implement JWT with golang-jwt/jwt/v5 +- Validate all tokens server-side + +### Input Validation +- Validate ALL user input using go-playground/validator +- Never trust client-side validation alone +- Sanitize output for context (HTML, SQL, shell) + +### Database Security +- Use parameterized queries ONLY - never string concatenation +- Validate ORDER BY columns against allowlist +- Use context with timeouts for all queries + +### Error Handling +- Never expose internal errors to users +- Log errors with slog including correlation IDs +- Return generic error messages to clients + +### Cryptography +- Use crypto/rand for all security-sensitive randomness +- TLS 1.3 minimum for all external connections +- No hardcoded secrets - use environment variables + +### HTTP Security +- Set ReadHeaderTimeout on all servers (Slowloris protection) +- Implement rate limiting for public endpoints +- Configure CORS restrictively + +## Security Tools +- Run `govulncheck ./...` before committing +- Run `gosec ./...` for static analysis +- All security findings must be addressed + +## Prohibited Patterns +- NO hardcoded credentials or API keys +- NO string concatenation in SQL queries +- NO shell command execution with user input +- NO disabled TLS certificate verification +- NO use of math/rand for security purposes +``` + +OWASP Go Security Subagent +-------------------------- + +Create a custom Claude Code command for security reviews. Save this as +`.claude/commands/security-review.md` in your project: + +```markdown +# Security Review + +Perform a comprehensive OWASP-focused security review of the Go code. + +## Review Checklist + +### 1. Input Validation (A03:2021) +- [ ] All user inputs validated with go-playground/validator +- [ ] Struct tags define validation rules +- [ ] Validation errors don't leak internal details + +### 2. Injection Prevention (A03:2021) +- [ ] SQL uses parameterized queries only +- [ ] No string concatenation with user input in queries +- [ ] Command execution uses exec.Command without shell +- [ ] Template rendering uses html/template not text/template + +### 3. Authentication (A07:2021) +- [ ] Passwords hashed with Argon2id +- [ ] JWT using golang-jwt/jwt/v5 (not dgrijalva) +- [ ] Token validation checks all claims +- [ ] Session cookies have Secure, HttpOnly, SameSite + +### 4. Cryptography (A02:2021) +- [ ] crypto/rand used for security randomness +- [ ] No weak algorithms (MD5, SHA1 for security) +- [ ] TLS 1.3 minimum configured +- [ ] Secrets loaded from environment/vault + +### 5. Error Handling +- [ ] Internal errors logged, not returned to users +- [ ] Panic recovery in HTTP handlers +- [ ] Context timeouts on all external calls + +### 6. Access Control (A01:2021) +- [ ] Path traversal prevented (filepath.Clean + validation) +- [ ] Rate limiting on sensitive endpoints +- [ ] CORS configured restrictively + +## How to Use + +Run this command: `/project:security-review` + +Claude will analyze the codebase against this checklist and report findings. +``` + +To use this subagent, run `/project:security-review` in Claude Code. + +Security Hooks Configuration +---------------------------- + +Configure Claude Code hooks to enforce security checks. Create +`.claude/settings.json`: + +```json +{ + "hooks": { + "preCommit": [ + { + "command": "govulncheck ./...", + "description": "Check for known vulnerabilities", + "blocking": true + }, + { + "command": "gosec -quiet ./...", + "description": "Static security analysis", + "blocking": true + } + ], + "postFileWrite": [ + { + "pattern": "*.go", + "command": "gofmt -w $FILE", + "description": "Format Go files" + } + ] + } +} +``` + +These hooks ensure security tools run automatically during development. + +Pre-Commit Hook Integration +--------------------------- + +For team-wide enforcement, use pre-commit hooks. Create `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/golangci/golangci-lint + rev: v1.55.2 + hooks: + - id: golangci-lint + args: ['--enable=gosec'] + + - repo: local + hooks: + - id: govulncheck + name: govulncheck + entry: govulncheck ./... + language: system + pass_filenames: false + types: [go] + + - id: go-test + name: go test + entry: go test -race ./... + language: system + pass_filenames: false + types: [go] +``` + +Install with: + +```bash +pip install pre-commit +pre-commit install +``` + +MCP Security Integrations +------------------------- + +Claude Code supports Model Context Protocol (MCP) servers for extended +capabilities. Security-relevant MCP integrations include: + +### Snyk MCP Server + +```json +{ + "mcpServers": { + "snyk": { + "command": "npx", + "args": ["@anthropic/snyk-mcp-server"], + "env": { + "SNYK_TOKEN": "${SNYK_TOKEN}" + } + } + } +} +``` + +This enables Claude to query Snyk's vulnerability database during code review. + +### Custom Security MCP Server + +For enterprise environments, create a custom MCP server that enforces +organization-specific security policies: + +```go +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" +) + +type SecurityCheck struct { + Name string `json:"name"` + Status string `json:"status"` + Detail string `json:"detail,omitempty"` +} + +func runGovulncheck() SecurityCheck { + cmd := exec.Command("govulncheck", "./...") + output, err := cmd.CombinedOutput() + if err != nil { + return SecurityCheck{ + Name: "govulncheck", + Status: "FAIL", + Detail: string(output), + } + } + return SecurityCheck{ + Name: "govulncheck", + Status: "PASS", + } +} + +func runGosec() SecurityCheck { + cmd := exec.Command("gosec", "-fmt=json", "-quiet", "./...") + output, err := cmd.CombinedOutput() + if err != nil { + return SecurityCheck{ + Name: "gosec", + Status: "FAIL", + Detail: string(output), + } + } + return SecurityCheck{ + Name: "gosec", + Status: "PASS", + } +} + +func main() { + checks := []SecurityCheck{ + runGovulncheck(), + runGosec(), + } + + result, _ := json.MarshalIndent(checks, "", " ") + fmt.Println(string(result)) + + // Exit with error if any check failed + for _, check := range checks { + if check.Status == "FAIL" { + os.Exit(1) + } + } +} +``` + +Security-Focused Prompting +-------------------------- + +When using Claude Code for Go development, include security context in prompts: + +### Good Prompts + +``` +"Add user registration with Argon2id password hashing and input validation" + +"Create a database query function using parameterized queries for PostgreSQL" + +"Implement JWT authentication using golang-jwt/jwt/v5 with proper claim validation" + +"Add rate limiting to the /api/login endpoint using golang.org/x/time/rate" +``` + +### Prompts to Avoid + +``` +"Add user registration" (no security context) + +"Query the database for this user" (might generate string concatenation) + +"Add authentication" (too vague, might use insecure patterns) +``` + +Automated Security Review Workflow +---------------------------------- + +Create a comprehensive security review workflow: + +### 1. Initial Code Generation + +``` +"Create a REST API endpoint for user creation following OWASP guidelines: +- Validate input with go-playground/validator +- Hash passwords with Argon2id +- Use parameterized queries +- Return generic errors to clients +- Log detailed errors server-side" +``` + +### 2. Security Review Request + +``` +"Review this code for OWASP Top 10 vulnerabilities: +- A01: Broken Access Control +- A02: Cryptographic Failures +- A03: Injection +- A07: Identification and Authentication Failures" +``` + +### 3. Fix Implementation + +``` +"Fix the identified security issues while maintaining functionality. +Explain each fix and the vulnerability it addresses." +``` + +CI/CD Security Gate +------------------- + +Integrate security checks into CI/CD pipelines: + +```yaml +# .github/workflows/security.yml +name: Security Checks + +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Install security tools + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run govulncheck + run: govulncheck ./... + + - name: Run gosec + run: gosec -fmt=sarif -out=gosec.sarif ./... + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: gosec.sarif + + - name: Run tests with race detector + run: go test -race ./... +``` + +Best Practices Summary +---------------------- + +| Practice | Recommendation | +|----------|----------------| +| CLAUDE.md | Always include security requirements | +| Custom commands | Create project-specific security review | +| Hooks | Automate govulncheck and gosec | +| Prompts | Include security context explicitly | +| Review | Request OWASP-focused code review | +| CI/CD | Gate deployments on security checks | + +References +---------- + +- [Claude Code Documentation][1] +- [OWASP Secure Coding Practices][2] +- [govulncheck][3] +- [gosec][4] + +[1]: https://docs.anthropic.com/en/docs/claude-code +[2]: https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/ +[3]: https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck +[4]: https://github.com/securego/gosec diff --git a/src/development-tools/security-scanning.md b/src/development-tools/security-scanning.md new file mode 100644 index 0000000..baa1e19 --- /dev/null +++ b/src/development-tools/security-scanning.md @@ -0,0 +1,443 @@ +Security Scanning Integration +============================= + +This section covers integrating security scanning tools into your Go development +workflow. Automated scanning catches vulnerabilities before they reach production. + +Essential Security Tools +------------------------ + +### govulncheck (Official Go Tool) + +The official vulnerability scanner from the Go team: + +```bash +# Install +go install golang.org/x/vuln/cmd/govulncheck@latest + +# Scan current module +govulncheck ./... + +# Scan specific package +govulncheck ./cmd/server + +# Output JSON for CI/CD +govulncheck -json ./... + +# Scan binary +govulncheck -mode=binary ./myapp +``` + +govulncheck checks against the Go vulnerability database and only reports +vulnerabilities in code paths actually used by your application. + +### gosec (Static Analysis) + +Security-focused static analyzer: + +```bash +# Install +go install github.com/securego/gosec/v2/cmd/gosec@latest + +# Basic scan +gosec ./... + +# Output formats +gosec -fmt=json -out=results.json ./... +gosec -fmt=sarif -out=results.sarif ./... +gosec -fmt=html -out=results.html ./... + +# Exclude specific rules +gosec -exclude=G104,G304 ./... + +# Only specific rules +gosec -include=G101,G102 ./... +``` + +Common gosec rules: + +| Rule | Description | +|------|-------------| +| G101 | Hardcoded credentials | +| G102 | Bind to all interfaces | +| G104 | Unhandled errors | +| G107 | URL provided to HTTP request from variable | +| G201 | SQL query with string format | +| G202 | SQL query with string concat | +| G301 | Poor file permissions | +| G304 | File path from variable (traversal) | +| G401 | Use of weak crypto (DES, MD5, SHA1) | +| G501 | Blocklisted imports | + +### golangci-lint (Meta Linter) + +Aggregates multiple linters including security tools: + +```bash +# Install +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Run with security linters +golangci-lint run --enable=gosec,govet,staticcheck +``` + +Configure `.golangci.yml`: + +```yaml +linters: + enable: + - gosec # Security scanner + - govet # Go vet + - staticcheck # Static analysis + - errcheck # Unchecked errors + - ineffassign # Unused assignments + - typecheck # Type checking + - bodyclose # Unclosed HTTP bodies + - noctx # HTTP requests without context + +linters-settings: + gosec: + includes: + - G101 # Hardcoded credentials + - G102 # Bind to all interfaces + - G103 # Audit unsafe block usage + - G104 # Audit errors not checked + - G106 # Audit SSH InsecureIgnoreHostKey + - G107 # URL provided to HTTP request + - G108 # Profiling endpoint exposed + - G109 # Integer overflow conversion + - G110 # Potential DoS from decompression + - G201 # SQL string formatting + - G202 # SQL string concatenation + - G203 # Unescaped HTML templates + - G204 # Audit command execution + - G301 # Poor file permissions (mkdir) + - G302 # Poor file permissions (chmod) + - G303 # Creating file with predictable name + - G304 # File path from tainted input + - G305 # Zip slip traversal + - G306 # Poor permissions on WriteFile + - G307 # Deferring Close on writable file + - G401 # Use of weak crypto + - G402 # TLS MinVersion too low + - G403 # RSA key < 2048 bits + - G404 # Use of weak PRNG + - G501 # Blocklisted import crypto/md5 + - G502 # Blocklisted import crypto/des + - G503 # Blocklisted import crypto/rc4 + - G504 # Blocklisted import net/http/cgi + - G505 # Blocklisted import crypto/sha1 + - G601 # Implicit memory aliasing + - G602 # Slice access out of bounds + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 +``` + +Go Fuzzing +---------- + +Go 1.18+ includes native fuzzing support: + +```go +// user_test.go +package user + +import ( + "testing" +) + +func FuzzValidateEmail(f *testing.F) { + // Seed corpus + f.Add("test@example.com") + f.Add("invalid-email") + f.Add("") + f.Add("a@b.c") + f.Add(" + + + + + +

Secure Page

+ + + + + +``` + +Report-Only Mode +---------------- + +Test your CSP without breaking functionality: + +```go +// Use Content-Security-Policy-Report-Only for testing +w.Header().Set("Content-Security-Policy-Report-Only", + "default-src 'self'; "+ + "report-uri /csp-report") +``` + +### Handling Reports + +```go +type CSPReport struct { + CSPReport struct { + DocumentURI string `json:"document-uri"` + Referrer string `json:"referrer"` + ViolatedDirective string `json:"violated-directive"` + EffectiveDirective string `json:"effective-directive"` + OriginalPolicy string `json:"original-policy"` + BlockedURI string `json:"blocked-uri"` + StatusCode int `json:"status-code"` + } `json:"csp-report"` +} + +func cspReportHandler(w http.ResponseWriter, r *http.Request) { + var report CSPReport + if err := json.NewDecoder(r.Body).Decode(&report); err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + // Log the violation for analysis + log.Printf("CSP Violation: %s blocked by %s on %s", + report.CSPReport.BlockedURI, + report.CSPReport.ViolatedDirective, + report.CSPReport.DocumentURI) + + w.WriteHeader(http.StatusNoContent) +} +``` + +Strict CSP Configuration +------------------------ + +For maximum security, use this strict configuration: + +```go +func strictCSP(nonce string) string { + return fmt.Sprintf( + "default-src 'none'; "+ // Block everything by default + "script-src 'nonce-%s' 'strict-dynamic'; "+ // Nonce-based scripts only + "style-src 'nonce-%s'; "+ // Nonce-based styles only + "img-src 'self'; "+ // Images from same origin + "font-src 'self'; "+ // Fonts from same origin + "connect-src 'self'; "+ // XHR/fetch to same origin + "form-action 'self'; "+ // Forms submit to same origin + "base-uri 'none'; "+ // No element + "frame-ancestors 'none'; "+ // Cannot be framed + "upgrade-insecure-requests", // Upgrade HTTP to HTTPS + nonce, nonce) +} +``` + +Common Pitfalls +--------------- + +1. **Using 'unsafe-inline'**: Defeats the purpose of CSP. Use nonces instead. + +2. **Overly permissive policies**: `script-src *` provides no protection. + +3. **Forgetting frame-ancestors**: Leaves you vulnerable to clickjacking. + +4. **Not using report-uri**: You won't know when attacks are blocked. + +5. **Missing base-uri**: Allows attackers to hijack relative URLs. + +Additional Security Headers +--------------------------- + +CSP works best with other security headers: + +```go +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // CSP (customize as shown above) + w.Header().Set("Content-Security-Policy", "...") + + // Prevent MIME sniffing + w.Header().Set("X-Content-Type-Options", "nosniff") + + // Clickjacking protection (older browsers) + w.Header().Set("X-Frame-Options", "DENY") + + // XSS filter (legacy browsers) + w.Header().Set("X-XSS-Protection", "1; mode=block") + + // Referrer policy + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Permissions policy (control browser features) + w.Header().Set("Permissions-Policy", + "geolocation=(), microphone=(), camera=()") + + next.ServeHTTP(w, r) + }) +} +``` + +References +---------- + +- [MDN: Content-Security-Policy][1] +- [OWASP CSP Cheat Sheet][2] +- [CSP Evaluator Tool][3] + +[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +[2]: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html +[3]: https://csp-evaluator.withgoogle.com/ diff --git a/src/output-encoding/cross-site-scripting.md b/src/output-encoding/cross-site-scripting.md index df3e65a..a2b6eb2 100644 --- a/src/output-encoding/cross-site-scripting.md +++ b/src/output-encoding/cross-site-scripting.md @@ -5,9 +5,10 @@ Although most developers have heard about it, most have never tried to exploit a Web Application using XSS. Cross Site Scripting has been on [OWASP Top 10][0] security risks since 2003 and -it's still a common vulnerability. The [2013 version][1] is quite detailed about -XSS, for example: attack vectors, security weakness, technical impacts and -business impacts. +it's still a common vulnerability. In the [2021 Top 10][1], XSS is now part of +the broader "Injection" category (A03:2021). The OWASP documentation is quite +detailed about XSS, covering: attack vectors, security weakness, technical +impacts and business impacts. In short @@ -151,10 +152,10 @@ but also `param1` is properly encoded to the output media: the browser. [html-template-plain-text]: images/html-template-plain-text.png [html-template-noxss]: images/html-template-text-plain-noxss.png -[0]: https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project -[1]: https://www.owasp.org/index.php/Top_10_2013-A3-Cross-Site_Scripting_(XSS) -[2]: https://golang.org/pkg/html/template/ -[3]: https://golang.org/pkg/net/http/ -[4]: https://golang.org/pkg/io/ -[5]: https://mimesniff.spec.whatwg.org/#rules-for-identifying-an-unknown-mime-typ -[6]: https://golang.org/pkg/text/template/ +[0]: https://owasp.org/Top10/ +[1]: https://owasp.org/Top10/A03_2021-Injection/ +[2]: https://pkg.go.dev/html/template +[3]: https://pkg.go.dev/net/http +[4]: https://pkg.go.dev/io +[5]: https://mimesniff.spec.whatwg.org/#rules-for-identifying-an-unknown-mime-type +[6]: https://pkg.go.dev/text/template diff --git a/src/output-encoding/sql-injection.md b/src/output-encoding/sql-injection.md index 2656f65..68f98ed 100644 --- a/src/output-encoding/sql-injection.md +++ b/src/output-encoding/sql-injection.md @@ -54,7 +54,264 @@ For example, comparing MySQL, PostgreSQL, and Oracle: | WHERE col = ? | WHERE col = $1 | WHERE col = :col | | VALUES(?, ?, ?) | VALUES($1, $2, $3) | VALUES(:val1, :val2, :val3) | +Complete Examples +----------------- + +### Basic CRUD Operations + +```go +// CREATE - Insert with parameters +func createUser(ctx context.Context, db *sql.DB, user *User) error { + query := `INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)` + _, err := db.ExecContext(ctx, query, user.Name, user.Email, time.Now()) + return err +} + +// READ - Select with parameters +func getUserByID(ctx context.Context, db *sql.DB, id int64) (*User, error) { + query := `SELECT id, name, email FROM users WHERE id = ?` + var user User + err := db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email) + if err == sql.ErrNoRows { + return nil, nil // Not found + } + return &user, err +} + +// UPDATE - Update with parameters +func updateUserEmail(ctx context.Context, db *sql.DB, id int64, email string) error { + query := `UPDATE users SET email = ?, updated_at = ? WHERE id = ?` + result, err := db.ExecContext(ctx, query, email, time.Now(), id) + if err != nil { + return err + } + rows, _ := result.RowsAffected() + if rows == 0 { + return errors.New("user not found") + } + return nil +} + +// DELETE - Delete with parameters +func deleteUser(ctx context.Context, db *sql.DB, id int64) error { + query := `DELETE FROM users WHERE id = ?` + _, err := db.ExecContext(ctx, query, id) + return err +} +``` + +### IN Clause Queries + +IN clauses require special handling: + +```go +// VULNERABLE - Don't do this! +func getUsersByIDsInsecure(db *sql.DB, ids []int64) ([]User, error) { + idsStr := strings.Join(int64sToStrings(ids), ",") + query := fmt.Sprintf("SELECT * FROM users WHERE id IN (%s)", idsStr) // Dangerous! + // ... +} + +// SECURE - Build parameters dynamically +func getUsersByIDs(ctx context.Context, db *sql.DB, ids []int64) ([]User, error) { + if len(ids) == 0 { + return nil, nil + } + + // Build placeholders: ?, ?, ? + placeholders := make([]string, len(ids)) + args := make([]interface{}, len(ids)) + for i, id := range ids { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id IN (%s)", + strings.Join(placeholders, ",")) + + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []User + for rows.Next() { + var u User + if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil { + return nil, err + } + users = append(users, u) + } + return users, rows.Err() +} +``` + +### Dynamic ORDER BY (Common Mistake) + +ORDER BY cannot use placeholders - validate against allowlist: + +```go +// VULNERABLE - Never do this! +func getUsersOrderedInsecure(db *sql.DB, orderBy string) ([]User, error) { + query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", orderBy) // SQL Injection! + // ... +} + +// SECURE - Allowlist validation +var allowedOrderColumns = map[string]string{ + "name": "name", + "email": "email", + "created_at": "created_at", + "id": "id", +} + +func getUsersOrdered(ctx context.Context, db *sql.DB, orderBy, direction string) ([]User, error) { + // Validate order column + column, ok := allowedOrderColumns[orderBy] + if !ok { + column = "id" // Safe default + } + + // Validate direction + if direction != "ASC" && direction != "DESC" { + direction = "ASC" // Safe default + } + + // Safe to use - validated values only + query := fmt.Sprintf("SELECT id, name, email FROM users ORDER BY %s %s", column, direction) + + rows, err := db.QueryContext(ctx, query) + // ... +} +``` + +### LIKE Queries + +Escape wildcard characters in LIKE patterns: + +```go +// SECURE - Escape user input for LIKE queries +func searchUsers(ctx context.Context, db *sql.DB, term string) ([]User, error) { + // Escape SQL LIKE wildcards + escaped := strings.ReplaceAll(term, "%", "\\%") + escaped = strings.ReplaceAll(escaped, "_", "\\_") + + query := `SELECT id, name, email FROM users WHERE name LIKE ? ESCAPE '\\'` + pattern := "%" + escaped + "%" + + rows, err := db.QueryContext(ctx, query, pattern) + // ... +} +``` + +### Using Prepared Statements + +For repeated queries, use prepared statements: + +```go +type UserRepository struct { + db *sql.DB + getByID *sql.Stmt + getByEmail *sql.Stmt +} + +func NewUserRepository(db *sql.DB) (*UserRepository, error) { + getByID, err := db.Prepare("SELECT id, name, email FROM users WHERE id = ?") + if err != nil { + return nil, err + } + + getByEmail, err := db.Prepare("SELECT id, name, email FROM users WHERE email = ?") + if err != nil { + getByID.Close() + return nil, err + } + + return &UserRepository{ + db: db, + getByID: getByID, + getByEmail: getByEmail, + }, nil +} + +func (r *UserRepository) GetByID(ctx context.Context, id int64) (*User, error) { + var user User + err := r.getByID.QueryRowContext(ctx, id).Scan(&user.ID, &user.Name, &user.Email) + if err == sql.ErrNoRows { + return nil, nil + } + return &user, err +} + +func (r *UserRepository) Close() { + r.getByID.Close() + r.getByEmail.Close() +} +``` + +### Using sqlx for Named Parameters + +The `sqlx` package provides named parameter support: + +```bash +go get github.com/jmoiron/sqlx +``` + +```go +import "github.com/jmoiron/sqlx" + +func createUserNamed(ctx context.Context, db *sqlx.DB, user *User) error { + query := `INSERT INTO users (name, email, created_at) + VALUES (:name, :email, :created_at)` + + _, err := db.NamedExecContext(ctx, query, map[string]interface{}{ + "name": user.Name, + "email": user.Email, + "created_at": time.Now(), + }) + return err +} +``` + +ORM Considerations +------------------ + +ORMs like GORM provide SQL injection protection by default: + +```go +import "gorm.io/gorm" + +// SECURE - GORM uses parameterized queries internally +func getUserByEmail(db *gorm.DB, email string) (*User, error) { + var user User + result := db.Where("email = ?", email).First(&user) + return &user, result.Error +} + +// VULNERABLE - Raw SQL with string concatenation +func getUserInsecure(db *gorm.DB, email string) (*User, error) { + var user User + // Don't do this! + db.Raw("SELECT * FROM users WHERE email = '" + email + "'").Scan(&user) + return &user, nil +} +``` + +Security Best Practices Summary +------------------------------- + +| Do | Don't | +|----|-------| +| Use parameterized queries | Concatenate user input into queries | +| Validate ORDER BY against allowlist | Use user input directly in ORDER BY | +| Escape LIKE wildcards | Trust ORM raw query methods | +| Use context with timeouts | Ignore query errors | +| Limit result counts | Return unlimited results | + Check the Database Security section in this guide to get more in-depth information about this topic. -[1]: https://golang.org/pkg/database/sql/#DB.Prepare +[1]: https://pkg.go.dev/database/sql#DB.Prepare +[2]: https://go.dev/doc/database/sql-injection +[3]: https://bobby-tables.com/go diff --git a/src/security-tooling/README.md b/src/security-tooling/README.md new file mode 100644 index 0000000..ddbb770 --- /dev/null +++ b/src/security-tooling/README.md @@ -0,0 +1,333 @@ +Security Tooling +================ + +Modern Go development benefits from a rich ecosystem of security tools. This +section covers essential tools that should be part of every Go project's +security workflow. + +Vulnerability Scanning with govulncheck +--------------------------------------- + +[govulncheck][1] is the official Go vulnerability scanner, maintained by the Go +security team. It analyzes your code and dependencies to find known +vulnerabilities. + +### Installation + +```bash +go install golang.org/x/vuln/cmd/govulncheck@latest +``` + +### Usage + +```bash +# Scan the current module +govulncheck ./... + +# Scan a specific package +govulncheck ./pkg/... + +# Scan in binary mode (analyzes compiled binary) +govulncheck -mode=binary ./myapp +``` + +### CI/CD Integration + +Add govulncheck to your CI pipeline to catch vulnerabilities early: + +```yaml +# GitHub Actions example +- name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + +- name: Run govulncheck + run: govulncheck ./... +``` + +govulncheck is more accurate than generic vulnerability scanners because it +analyzes actual code paths - it won't report vulnerabilities in code your +application doesn't use. + +Static Analysis with gosec +-------------------------- + +[gosec][2] (Go Security Checker) scans Go source code for common security +issues such as: + +- Hardcoded credentials +- SQL injection +- Command injection +- Weak cryptographic primitives +- Insecure file permissions + +### Installation + +```bash +go install github.com/securego/gosec/v2/cmd/gosec@latest +``` + +### Usage + +```bash +# Scan the current directory +gosec ./... + +# Output as JSON +gosec -fmt=json -out=results.json ./... + +# Exclude specific rules +gosec -exclude=G104 ./... +``` + +### Common Rules + +| Rule ID | Description | +|---------|-------------| +| G101 | Hardcoded credentials | +| G102 | Bind to all interfaces | +| G103 | Unsafe block | +| G104 | Audit errors not checked | +| G201-G203 | SQL injection | +| G301-G307 | File permission issues | +| G401-G407 | Weak cryptographic primitives | +| G501-G505 | Import blocklist (crypto/md5, etc.) | + +Race Detection +-------------- + +Go's built-in race detector finds data races in concurrent code. Data races +are a common source of security vulnerabilities and unpredictable behavior. + +### Usage + +```bash +# Run tests with race detection +go test -race ./... + +# Build with race detection (for testing only - adds overhead) +go build -race -o myapp-race ./cmd/myapp + +# Run the race-enabled binary +./myapp-race +``` + +### Example + +```go +// INSECURE - Data race +var counter int + +func increment() { + counter++ // Race condition! +} + +func main() { + go increment() + go increment() + // counter may be 1 or 2 depending on timing +} + +// SECURE - Use synchronization +var ( + counter int + mu sync.Mutex +) + +func increment() { + mu.Lock() + defer mu.Unlock() + counter++ +} +``` + +**Important:** The race detector adds significant overhead (2-10x slower, +5-10x more memory). Use it in testing, not production. + +Fuzz Testing +------------ + +Go 1.18 introduced native fuzzing support. Fuzzing automatically generates +test inputs to find edge cases and security issues like buffer overflows, +panics, and injection vulnerabilities. + +### Writing a Fuzz Test + +```go +// In your_test.go file +func FuzzParseInput(f *testing.F) { + // Add seed corpus (known valid inputs) + f.Add("valid input") + f.Add("another valid input") + f.Add("") + f.Add("") + + f.Fuzz(func(t *testing.T, input string) { + // Call the function under test + result, err := ParseInput(input) + + // The fuzzer will try to find inputs that cause: + // - Panics + // - Hangs + // - Unexpected behavior + + if err != nil { + return // Expected errors are fine + } + + // Add invariant checks + if result == nil { + t.Error("result should not be nil when err is nil") + } + }) +} +``` + +### Running Fuzz Tests + +```bash +# Run for 30 seconds +go test -fuzz=FuzzParseInput -fuzztime=30s ./... + +# Run until stopped (Ctrl+C) +go test -fuzz=FuzzParseInput ./... + +# Run with specific seed corpus +go test -fuzz=FuzzParseInput -fuzztime=1m ./... -test.fuzzcachedir=testdata/fuzz +``` + +### Security-Focused Fuzzing + +Focus on parsing and input handling functions: + +```go +func FuzzJSONDecoder(f *testing.F) { + f.Add([]byte(`{"key": "value"}`)) + f.Add([]byte(`{"key": 123}`)) + f.Add([]byte(`[1, 2, 3]`)) + + f.Fuzz(func(t *testing.T, data []byte) { + var result map[string]interface{} + // Should not panic on any input + _ = json.Unmarshal(data, &result) + }) +} +``` + +Supply Chain Security +--------------------- + +Securing your dependencies is critical. Go provides several mechanisms for +supply chain security. + +### Module Verification + +```bash +# Verify all dependencies match go.sum checksums +go mod verify + +# Download and verify all dependencies +go mod download -x +``` + +### Environment Variables + +Configure Go's module behavior for security: + +```bash +# Use the public module proxy (default) +export GOPROXY="https://proxy.golang.org,direct" + +# Use checksum database for verification (default) +export GOSUMDB="sum.golang.org" + +# For private modules, configure GOPRIVATE +export GOPRIVATE="github.com/your-org/*" +``` + +### SBOM Generation + +Generate a Software Bill of Materials for your project: + +```bash +# Using go version -m (basic) +go version -m ./myapp + +# Using cyclonedx-gomod (detailed SBOM) +go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest +cyclonedx-gomod mod -json -output sbom.json +``` + +### Dependency Updates + +Regularly update dependencies and audit for vulnerabilities: + +```bash +# Check for available updates +go list -m -u all + +# Update all dependencies +go get -u ./... + +# Update to latest minor/patch versions only +go get -u=patch ./... + +# Then scan for vulnerabilities +govulncheck ./... +``` + +### Vendoring (Optional) + +For additional supply chain control, vendor your dependencies: + +```bash +# Create vendor directory +go mod vendor + +# Build using vendor +go build -mod=vendor ./... +``` + +Recommended CI Pipeline +----------------------- + +A security-focused CI pipeline should include: + +```yaml +name: Security Checks + +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Verify dependencies + run: go mod verify + + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + + - name: Run gosec + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + gosec ./... + + - name: Run tests with race detector + run: go test -race ./... + + - name: Run fuzz tests (time-limited) + run: | + go test -fuzz=. -fuzztime=30s ./... +``` + +[1]: https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck +[2]: https://github.com/securego/gosec diff --git a/src/session-management/README.md b/src/session-management/README.md index 6a9960d..8bf78a1 100644 --- a/src/session-management/README.md +++ b/src/session-management/README.md @@ -23,33 +23,52 @@ func setToken(res http.ResponseWriter, req *http.Request) { We must ensure that the algorithms used to generate our session identifier are sufficiently random, to prevent session brute forcing. +**Important:** Never hardcode secrets in source code. Use environment variables: + ```go -... +// INSECURE - Never do this +signedToken, _ := token.SignedString([]byte("secret")) + +// SECURE - Use environment variables +secret := os.Getenv("JWT_SECRET") +if secret == "" { + log.Fatal("JWT_SECRET environment variable required") +} token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) -signedToken, _ := token.SignedString([]byte("secret")) //our secret -... +signedToken, err := token.SignedString([]byte(secret)) +if err != nil { + // Handle error - never ignore errors from cryptographic operations + log.Printf("Error signing token: %v", err) + return +} ``` Now that we have a sufficiently strong token, we must also set -the `Domain`, `Path`, `Expires`, `HTTP only`, `Secure` for our cookies. In this -case the `Expires` value is in this example set to 30 minutes since we are +the `Domain`, `Path`, `Expires`, `HttpOnly`, `Secure`, and `SameSite` for our +cookies. In this example the `Expires` value is set to 30 minutes since we are considering our application a low-risk application. ```go // Our cookie parameter cookie := http.Cookie{ - Name: "Auth", - Value: signedToken, - Expires: expireCookie, - HttpOnly: true, - Path: "/", - Domain: "127.0.0.1", - Secure: true + Name: "Auth", + Value: signedToken, + Expires: expireCookie, // 30 min + HttpOnly: true, // Prevents JavaScript access (mitigates XSS) + Path: "/", + Domain: "127.0.0.1", + Secure: true, // Only sent over HTTPS + SameSite: http.SameSiteStrictMode, // CSRF protection } -http.SetCookie(res, &cookie) //Set the cookie +http.SetCookie(res, &cookie) // Set the cookie ``` +The `SameSite` attribute is critical for CSRF protection. Go supports three values: +- `http.SameSiteStrictMode` - Cookie is only sent in first-party context (recommended for auth cookies) +- `http.SameSiteLaxMode` - Cookie is sent with top-level navigations and GET requests from third-party sites +- `http.SameSiteNoneMode` - Cookie is sent in all contexts (requires `Secure: true`) + Upon sign-in, a new session is always generated. The old session is never re-used, even if it is not expired. We also use the `Expire` parameter to enforce periodic session termination as a diff --git a/src/session-management/session.go b/src/session-management/session.go index a5bed9f..2f49ed2 100644 --- a/src/session-management/session.go +++ b/src/session-management/session.go @@ -5,10 +5,11 @@ import ( "fmt" "log" "net/http" + "os" "time" - // JWT is not in the native Go packages - "github.com/dgrijalva/jwt-go" + // Maintained JWT library (replaces deprecated dgrijalva/jwt-go) + "github.com/golang-jwt/jwt/v5" ) type Key int @@ -18,36 +19,52 @@ const MyKey Key = 0 // JWT schema of the data it will store type Claims struct { Username string `json:"username"` - jwt.StandardClaims + jwt.RegisteredClaims +} + +// getJWTSecret retrieves the JWT secret from environment variables. +// SECURITY: Never hardcode secrets in source code. +func getJWTSecret() []byte { + secret := os.Getenv("JWT_SECRET") + if secret == "" { + log.Fatal("JWT_SECRET environment variable is required") + } + return []byte(secret) } // create a JWT and put in the clients cookie func setToken(res http.ResponseWriter, req *http.Request) { - // 30m Expiration for non-sensitive applications - OWSAP - expireToken := time.Now().Add(time.Minute * 30).Unix() + // 30m Expiration for non-sensitive applications - OWASP expireCookie := time.Now().Add(time.Minute * 30) - // token Claims + // token Claims using jwt/v5 RegisteredClaims claims := Claims{ - "TestUser", - jwt.StandardClaims{ - ExpiresAt: expireToken, + Username: "TestUser", + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expireCookie), Issuer: "localhost:9000", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, _ := token.SignedString([]byte("secret")) + signedToken, err := token.SignedString(getJWTSecret()) + if err != nil { + http.Error(res, "Internal server error", http.StatusInternalServerError) + log.Printf("Error signing token: %v", err) + return + } // Set Cookie parameters + // SameSite attribute helps prevent CSRF attacks cookie := http.Cookie{ Name: "Auth", Value: signedToken, Expires: expireCookie, // 30 min - HttpOnly: true, + HttpOnly: true, // Prevents JavaScript access Path: "/", Domain: "127.0.0.1", - Secure: true, + Secure: true, // Only sent over HTTPS + SameSite: http.SameSiteStrictMode, // CSRF protection } http.SetCookie(res, &cookie) @@ -70,7 +87,7 @@ func validate(page http.HandlerFunc) http.HandlerFunc { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } - return []byte("secret"), nil + return getJWTSecret(), nil }, ) diff --git a/src/system-configuration/container-security.md b/src/system-configuration/container-security.md new file mode 100644 index 0000000..e4488e7 --- /dev/null +++ b/src/system-configuration/container-security.md @@ -0,0 +1,409 @@ +Container Security +================== + +Containerizing Go applications requires attention to security at every layer. +This section covers secure Docker images, runtime configuration, and +Kubernetes deployment best practices. + +Minimal Container Images +------------------------ + +Go's static binary compilation enables extremely small, secure containers: + +### Multi-Stage Build (Recommended) + +```dockerfile +# Build stage +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +# Copy dependency files first (better layer caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source and build +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -trimpath \ + -ldflags="-w -s -extldflags '-static'" \ + -o /app/server \ + ./cmd/server + +# Final stage - minimal image +FROM scratch + +# Copy CA certificates for HTTPS +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy timezone data (if needed) +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo + +# Copy binary +COPY --from=builder /app/server /server + +# Run as non-root user +USER 65534:65534 + +EXPOSE 8080 + +ENTRYPOINT ["/server"] +``` + +### Build Flags Explained + +| Flag | Purpose | +|------|---------| +| `CGO_ENABLED=0` | Disable cgo for static binary | +| `GOOS=linux` | Target Linux | +| `GOARCH=amd64` | Target architecture | +| `-trimpath` | Remove file paths from binary | +| `-ldflags="-w -s"` | Strip debug info and symbol table | +| `-extldflags '-static'` | Force static linking | + +### Image Size Comparison + +| Base Image | Typical Size | Attack Surface | +|------------|--------------|----------------| +| `ubuntu:22.04` | ~77MB | High | +| `alpine:3.19` | ~7MB | Medium | +| `gcr.io/distroless/static` | ~2MB | Low | +| `scratch` | 0MB | Minimal | + +Non-Root Execution +------------------ + +Never run containers as root: + +### Using scratch (No User Database) + +```dockerfile +# In scratch images, use numeric UID/GID +USER 65534:65534 # nobody:nogroup +``` + +### Using distroless + +```dockerfile +FROM gcr.io/distroless/static:nonroot +COPY --from=builder /app/server /server +USER nonroot:nonroot +ENTRYPOINT ["/server"] +``` + +### Using Alpine + +```dockerfile +FROM alpine:3.19 + +# Create non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +COPY --from=builder /app/server /server + +USER appuser:appgroup +ENTRYPOINT ["/server"] +``` + +Security Scanning +----------------- + +Scan images for vulnerabilities before deployment: + +### In CI/CD Pipeline + +```yaml +# GitHub Actions example +- name: Build image + run: docker build -t myapp:${{ github.sha }} . + +- name: Scan with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: myapp:${{ github.sha }} + format: 'table' + exit-code: '1' + severity: 'CRITICAL,HIGH' + +- name: Scan with Snyk + uses: snyk/actions/docker@master + with: + image: myapp:${{ github.sha }} + args: --severity-threshold=high +``` + +### Local Scanning + +```bash +# Using Trivy +trivy image myapp:latest + +# Using Grype +grype myapp:latest + +# Using Docker Scout +docker scout cves myapp:latest +``` + +Kubernetes Security +------------------- + +### Pod Security Standards + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: secure-app +spec: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + fsGroup: 65534 + seccompProfile: + type: RuntimeDefault + + containers: + - name: app + image: myapp:latest + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + + resources: + limits: + cpu: "500m" + memory: "256Mi" + requests: + cpu: "100m" + memory: "128Mi" + + # Health checks + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +### Network Policies + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: app-network-policy +spec: + podSelector: + matchLabels: + app: myapp + policyTypes: + - Ingress + - Egress + + ingress: + - from: + - podSelector: + matchLabels: + role: frontend + ports: + - protocol: TCP + port: 8080 + + egress: + - to: + - podSelector: + matchLabels: + role: database + ports: + - protocol: TCP + port: 5432 + # Allow DNS + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 +``` + +### Secrets Management + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secrets +type: Opaque +stringData: + database-url: "postgres://user:pass@db:5432/app" + api-key: "secret-key-here" + +--- +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: app + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: app-secrets + key: database-url + - name: API_KEY + valueFrom: + secretKeyRef: + name: app-secrets + key: api-key +``` + +Health Endpoints in Go +---------------------- + +Implement proper health checks: + +```go +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "sync/atomic" +) + +var ( + ready int32 = 0 + db *sql.DB +) + +// /health - Liveness probe +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +// /ready - Readiness probe +func readyHandler(w http.ResponseWriter, r *http.Request) { + if atomic.LoadInt32(&ready) == 0 { + http.Error(w, "Not ready", http.StatusServiceUnavailable) + return + } + + // Check dependencies + if err := db.Ping(); err != nil { + http.Error(w, "Database unavailable", http.StatusServiceUnavailable) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Ready")) +} + +func main() { + // Initialize dependencies... + db = initDatabase() + + // Mark as ready when initialization complete + atomic.StoreInt32(&ready, 1) + + mux := http.NewServeMux() + mux.HandleFunc("/health", healthHandler) + mux.HandleFunc("/ready", readyHandler) + // ... +} +``` + +Docker Compose Security +----------------------- + +For local development: + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + user: "65534:65534" + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp:noexec,nosuid,size=64m + environment: + - DB_HOST=db + secrets: + - db_password + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16-alpine + user: "999:999" + environment: + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + secrets: + - db_password + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +secrets: + db_password: + file: ./secrets/db_password.txt + +volumes: + db_data: +``` + +Best Practices Summary +---------------------- + +| Practice | Recommendation | +|----------|----------------| +| Base image | Use `scratch` or `distroless` | +| User | Run as non-root (UID 65534) | +| Filesystem | Read-only root filesystem | +| Capabilities | Drop ALL, add only what's needed | +| Network | Use network policies to restrict traffic | +| Secrets | Use Kubernetes secrets or secret managers | +| Scanning | Scan images in CI/CD before deployment | +| Resources | Set CPU/memory limits | +| Health checks | Implement liveness and readiness probes | + +References +---------- + +- [Docker Security Best Practices][1] +- [Kubernetes Pod Security Standards][2] +- [Google Distroless Images][3] +- [Trivy Scanner][4] + +[1]: https://docs.docker.com/develop/security-best-practices/ +[2]: https://kubernetes.io/docs/concepts/security/pod-security-standards/ +[3]: https://github.com/GoogleContainerTools/distroless +[4]: https://github.com/aquasecurity/trivy