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