[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# Security Architecture Patterns
|
||||
|
||||
Defense-in-depth, Zero Trust, and authentication patterns for Go services.
|
||||
|
||||
## Defense-in-Depth Layers
|
||||
|
||||
Multiple security controls ensure that failure of one layer doesn't compromise the system:
|
||||
|
||||
```
|
||||
Layer 1: PERIMETER — Rate limiting, DDoS mitigation, WAF
|
||||
Layer 2: NETWORK — TLS/mTLS, network segmentation
|
||||
Layer 3: APPLICATION — Input validation, auth, authz, secure coding
|
||||
Layer 4: DATA — Encryption at rest/transit, access controls, backups
|
||||
```
|
||||
|
||||
### Go Implementation by Layer
|
||||
|
||||
**Layer 1 — Rate Limiting Middleware:**
|
||||
|
||||
```go
|
||||
import "golang.org/x/time/rate"
|
||||
|
||||
// Global rate limiter
|
||||
func RateLimitMiddleware(rps float64, burst int) func(http.Handler) http.Handler {
|
||||
limiter := rate.NewLimiter(rate.Limit(rps), burst)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !limiter.Allow() {
|
||||
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Per-client rate limiting** prevents a single abuser from exhausting the global limit:
|
||||
|
||||
```go
|
||||
type ClientRateLimiter struct {
|
||||
mu sync.Mutex
|
||||
clients map[string]*rate.Limiter
|
||||
rps rate.Limit
|
||||
burst int
|
||||
}
|
||||
|
||||
func (crl *ClientRateLimiter) GetLimiter(clientIP string) *rate.Limiter {
|
||||
crl.mu.Lock()
|
||||
defer crl.mu.Unlock()
|
||||
if limiter, exists := crl.clients[clientIP]; exists {
|
||||
return limiter
|
||||
}
|
||||
limiter := rate.NewLimiter(crl.rps, crl.burst)
|
||||
crl.clients[clientIP] = limiter
|
||||
return limiter
|
||||
}
|
||||
```
|
||||
|
||||
**Layer 2 — mTLS for Service-to-Service:**
|
||||
|
||||
```go
|
||||
func mTLSConfig(caCertFile, clientCertFile, clientKeyFile string) (*tls.Config, error) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCert, err := os.ReadFile(caCertFile)
|
||||
if err != nil { return nil, err }
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
|
||||
if err != nil { return nil, err }
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Layer 3 — Request Body Size Limiting:**
|
||||
|
||||
```go
|
||||
func MaxBodySize(maxBytes int64) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Layer 4 — Encryption at Rest (AES-GCM):**
|
||||
|
||||
Use `crypto/aes` with GCM mode for authenticated encryption. See [Cryptography Security](./cryptography.md) for full `EncryptAESGCM`/`DecryptAESGCM` implementations, algorithm selection guide, and envelope encryption for key rotation.
|
||||
|
||||
---
|
||||
|
||||
## Zero Trust Principles
|
||||
|
||||
| Principle | Implementation |
|
||||
| --- | --- |
|
||||
| Verify explicitly | Authenticate and authorize every request — no implicit trust from network location |
|
||||
| Least privilege | Grant minimum permissions; use short-lived tokens (15min access, 7d refresh) |
|
||||
| Assume breach | Segment services, encrypt all communication, log all access for anomaly detection |
|
||||
|
||||
```go
|
||||
// Zero Trust middleware: verify identity + permissions on every request
|
||||
func ZeroTrustMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Verify token
|
||||
claims, err := validateJWT(r.Header.Get("Authorization"))
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// 2. Verify permissions for this specific resource
|
||||
if !hasPermission(claims.Subject, r.Method, r.URL.Path) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// 3. Audit log
|
||||
logger.Info("access_granted",
|
||||
"user", claims.Subject,
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"ip", r.RemoteAddr,
|
||||
)
|
||||
ctx := context.WithValue(r.Context(), userClaimsKey, claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Pattern Selection
|
||||
|
||||
| Use Case | Recommended Pattern | Go Implementation |
|
||||
| --- | --- | --- |
|
||||
| Web application | OAuth 2.0 + PKCE with OIDC | `golang.org/x/oauth2` |
|
||||
| API authentication | JWT with short expiry + refresh tokens | `github.com/golang-jwt/jwt/v5` |
|
||||
| Service-to-service | mTLS with certificate rotation | `crypto/tls` with `tls.LoadX509KeyPair` |
|
||||
| CLI/Automation | API keys with IP allowlisting | Custom middleware with `net.ParseIP` |
|
||||
| High security | FIDO2/WebAuthn hardware keys | `github.com/go-webauthn/webauthn` |
|
||||
|
||||
### JWT Validation — Complete Example
|
||||
|
||||
JWT validation must pin the signing algorithm to prevent algorithm confusion attacks (where an attacker switches RS256 to HS256 and signs with the public key):
|
||||
|
||||
```go
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
func validateJWT(authHeader string) (*jwt.RegisteredClaims, error) {
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
// Pin signing algorithm — prevents algorithm confusion
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return publicKey, nil
|
||||
},
|
||||
jwt.WithIssuer("your-issuer"),
|
||||
jwt.WithAudience("your-audience"),
|
||||
jwt.WithExpirationRequired(),
|
||||
)
|
||||
if err != nil { return nil, err }
|
||||
claims, ok := token.Claims.(*jwt.RegisteredClaims)
|
||||
if !ok { return nil, errors.New("invalid claims") }
|
||||
return claims, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Password Hashing — Argon2id
|
||||
|
||||
Argon2id is the recommended password hashing algorithm (memory-hard, resists GPU attacks). For algorithm comparison (bcrypt, scrypt, PBKDF2), see [Cryptography Security](./cryptography.md).
|
||||
|
||||
```go
|
||||
import "golang.org/x/crypto/argon2"
|
||||
|
||||
type PasswordConfig struct {
|
||||
Time uint32 // iterations
|
||||
Memory uint32 // KB
|
||||
Threads uint8
|
||||
KeyLen uint32
|
||||
SaltLen uint32
|
||||
}
|
||||
|
||||
// OWASP recommended parameters
|
||||
var DefaultConfig = PasswordConfig{
|
||||
Time: 3, Memory: 64 * 1024, Threads: 4, KeyLen: 32, SaltLen: 16,
|
||||
}
|
||||
|
||||
func HashPassword(password string, cfg PasswordConfig) (string, error) {
|
||||
salt := make([]byte, cfg.SaltLen)
|
||||
if _, err := rand.Read(salt); err != nil { return "", err }
|
||||
hash := argon2.IDKey([]byte(password), salt, cfg.Time, cfg.Memory, cfg.Threads, cfg.KeyLen)
|
||||
// Encode salt + hash for storage
|
||||
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version, cfg.Memory, cfg.Time, cfg.Threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(hash),
|
||||
), nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP Security Headers
|
||||
|
||||
Set on every response via middleware:
|
||||
|
||||
```go
|
||||
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
| Header | Purpose | Recommended Value |
|
||||
| --- | --- | --- |
|
||||
| Content-Security-Policy | Prevents XSS by restricting resource sources | `default-src 'self'; script-src 'self'` |
|
||||
| X-Frame-Options | Prevents clickjacking via framing | `DENY` |
|
||||
| X-Content-Type-Options | Prevents MIME-type sniffing | `nosniff` |
|
||||
| Strict-Transport-Security | Forces HTTPS, prevents protocol downgrade | `max-age=31536000; includeSubDomains` |
|
||||
| Referrer-Policy | Controls referrer header leakage | `strict-origin-when-cross-origin` |
|
||||
| Permissions-Policy | Restricts browser features (camera, mic, geolocation) | `geolocation=(), microphone=(), camera=()` |
|
||||
|
||||
---
|
||||
|
||||
## Security Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why It Fails | Go Fix |
|
||||
| --- | --- | --- |
|
||||
| Security through obscurity | Hidden admin URLs are discoverable via fuzzing, logs, or source code | Authentication + authorization on all endpoints |
|
||||
| Trusting client headers | `X-Forwarded-For`, `X-Is-Admin` — clients forge any header | Server-side identity verification; trust proxy headers only from known load balancers |
|
||||
| Client-side authorization | JavaScript checks are trivially bypassed by any HTTP client | Server-side `if !user.HasRole("admin")` on every protected handler |
|
||||
| Shared secrets across environments | Staging breach → production compromise | Per-environment secrets via secret manager |
|
||||
| Catching and ignoring crypto errors | `_, _ = encrypt(data)` silently proceeds with unencrypted data | Always check error returns — fail closed, never open |
|
||||
| Rolling your own crypto | Custom encryption hasn't been analyzed by cryptographers | Use `crypto/aes` GCM, `golang.org/x/crypto/argon2` |
|
||||
| Verbose error responses | Stack traces and DB errors reveal internals to attackers | Generic errors to clients (`http.Error(w, "Internal error", 500)`), detailed logs server-side |
|
||||
|
||||
```go
|
||||
// Anti-pattern: trusting client-provided identity
|
||||
func badHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-Is-Admin") == "true" { // attacker sets this header
|
||||
adminPanel(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Correct: server-side identity verification
|
||||
func goodHandler(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(userClaimsKey).(*jwt.RegisteredClaims)
|
||||
if !hasRole(claims.Subject, "admin") {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
adminPanel(w, r)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# Security Review Checklist
|
||||
|
||||
Severity: Critical, High, Medium, Low
|
||||
|
||||
## Input Handling
|
||||
|
||||
- [ ] **High** All user input validated at system boundaries — internal code trusts the boundary
|
||||
- [ ] **High** Input uses allowlists, not blocklists — blocklists always miss something
|
||||
- [ ] **High** Sanitized on output (HTML, SQL, shell) — context-dependent escaping
|
||||
- [ ] **Medium** Length limits enforced — prevents buffer abuse and DoS
|
||||
|
||||
## Database
|
||||
|
||||
- [ ] **Critical** SQL queries use parameterized placeholders — keeps data and code separate
|
||||
- [ ] **Critical** ORM/library protects against SQL injection
|
||||
- [ ] **Critical** No direct SQL construction with user input
|
||||
|
||||
## Code Execution
|
||||
|
||||
- [ ] **Critical** No `exec.Command()` with shell arguments — metacharacters enable injection
|
||||
- [ ] **Critical** No eval, reflection on untrusted input — arbitrary code execution risk
|
||||
- [ ] **Critical** No deserialization of untrusted data — can trigger arbitrary constructors
|
||||
|
||||
## Cryptography
|
||||
|
||||
- [ ] **High** Uses `crypto/rand` for security-critical randomness — `math/rand` is predictable
|
||||
- [ ] **High** Uses vetted algorithms (AES-GCM, Argon2id, bcrypt) — custom crypto hasn't been analyzed
|
||||
- [ ] **Critical** Proper key management — hardcoded secrets leak through VCS, logs, and backups
|
||||
- [ ] **Medium** HMAC for message authentication — prevents tampering
|
||||
|
||||
## Web Security
|
||||
|
||||
- [ ] **High** TLS 1.2+ configured correctly — older versions have known attacks
|
||||
- [ ] **Medium** Security headers set (HSTS, CSP, X-Frame-Options) — prevents framing, sniffing, downgrade
|
||||
- [ ] **Medium** CSRF protection for state-changing requests — prevents cross-origin action forgery
|
||||
- [ ] **Medium** Open redirects validated — attackers use your domain to redirect to phishing
|
||||
- [ ] **High** XSS protected via `html/template` auto-escaping
|
||||
|
||||
## Authentication/Authorization
|
||||
|
||||
- [ ] **High** Passwords hashed with Argon2id (preferred) or bcrypt — intentionally slow to resist brute-force
|
||||
- [ ] **High** Sessions use secure tokens from `crypto/rand`
|
||||
- [ ] **High** Authorization checked on every privileged action — not just at login
|
||||
- [ ] **High** JWT tokens validated (algorithm, claims, expiry) — unsigned JWTs bypass auth
|
||||
- [ ] **High** Expired/invalid sessions invalidated server-side
|
||||
|
||||
## Error Handling
|
||||
|
||||
- [ ] **Medium** Generic error messages to users — detailed errors help attackers map your system
|
||||
- [ ] **Medium** Detailed errors logged server-side only
|
||||
- [ ] **Medium** Stack traces not leaked to clients
|
||||
- [ ] **Medium** Database errors not exposed — reveals schema and query structure
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- [ ] **High** `govulncheck` passes — catches known CVEs in your dependency tree
|
||||
- [ ] **High** Dependencies updated regularly — unpatched deps are the #1 attack vector
|
||||
- [ ] **Medium** Third-party libraries reviewed for security posture
|
||||
|
||||
## HTTP Security Headers
|
||||
|
||||
- [ ] **Medium** `Content-Security-Policy` set — restricts resource sources to prevent XSS
|
||||
- [ ] **Medium** `X-Frame-Options: DENY` — prevents clickjacking via iframe embedding
|
||||
- [ ] **Medium** `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing attacks
|
||||
- [ ] **Medium** `Strict-Transport-Security` with `includeSubDomains` — forces HTTPS, prevents downgrade
|
||||
- [ ] **Low** `Referrer-Policy` set — controls referrer header leakage to external sites
|
||||
- [ ] **Low** `Permissions-Policy` set — restricts browser features (camera, mic, geolocation)
|
||||
|
||||
## Rate Limiting & DoS Prevention
|
||||
|
||||
- [ ] **Medium** HTTP server has `ReadTimeout`, `WriteTimeout`, `IdleTimeout` — prevents Slowloris
|
||||
- [ ] **Medium** Request body size limited with `http.MaxBytesReader` — prevents memory exhaustion
|
||||
- [ ] **Medium** Rate limiting on authentication endpoints — prevents brute-force and credential stuffing
|
||||
- [ ] **Medium** Rate limiting on expensive operations (search, export, file upload)
|
||||
|
||||
## Concurrency
|
||||
|
||||
- [ ] **High** `-race` detector passes — races cause data corruption and can bypass auth checks
|
||||
- [ ] **High** Shared state properly synchronized
|
||||
- [ ] **High** No data races on global variables
|
||||
@@ -0,0 +1,200 @@
|
||||
# Cookie Security Rules
|
||||
|
||||
Cookie security is critical for preventing session hijacking and XSS exploitation.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. Cookies MUST set `HttpOnly` for session and authentication cookies.
|
||||
2. Cookies MUST set `Secure` in production (HTTPS only).
|
||||
3. `SameSite` SHOULD be `Lax` or `Strict` — use `None` only when cross-site access is required.
|
||||
|
||||
---
|
||||
|
||||
## HTTP-Only Flag Missing — Medium
|
||||
|
||||
Without HttpOnly flag, cookies can be accessed via JavaScript.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
cookie := &http.Cookie{
|
||||
Name: "session",
|
||||
Value: sessionID,
|
||||
// DON'T: Missing HttpOnly, Secure flags
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
cookie := &http.Cookie{
|
||||
Name: "session",
|
||||
Value: sessionID,
|
||||
HttpOnly: true, // Prevents JavaScript access
|
||||
Secure: true, // Only sends over HTTPS
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Path: "/",
|
||||
MaxAge: 3600,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure Cookie Configuration (Missing Secure Flag) — Medium
|
||||
|
||||
Without Secure flag, cookies are sent over unencrypted HTTP.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
// Missing Secure, HttpOnly flags
|
||||
})
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
Secure: true, // HTTPS only
|
||||
HttpOnly: true, // No JavaScript access
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
MaxAge: 86400,
|
||||
Domain: "", // Default: send to exact host only
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SameSite Cookie Protection — Medium
|
||||
|
||||
SameSite attribute protects against CSRF attacks.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
cookie := &http.Cookie{
|
||||
Name: "session",
|
||||
Value: token,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
// DON'T: Missing SameSite
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Strict for high-security operations
|
||||
authCookie := &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: token,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
|
||||
// Lax for most applications
|
||||
sessionCookie := &http.Cookie{
|
||||
Name: "session",
|
||||
Value: token,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
// None for cross-site cookies (requires Secure: true)
|
||||
crossSiteCookie := &http.Cookie{
|
||||
Name: "analytics",
|
||||
Value: trackingID,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cookie Prefix Examples — Low
|
||||
|
||||
Modern cookie prefixes enforce cookie behavior in browsers.
|
||||
|
||||
```go
|
||||
// __Secure- prefix: Requires Secure flag
|
||||
secureCookie := &http.Cookie{
|
||||
Name: "__Secure-Session",
|
||||
Value: token,
|
||||
Secure: true, // Required for __Secure-
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
// __Host- prefix: Requires Secure, no Domain, origin-bound path
|
||||
hostCookie := &http.Cookie{
|
||||
Name: "__Host-CSRF",
|
||||
Value: csrfToken,
|
||||
Secure: true, // Required
|
||||
HttpOnly: true,
|
||||
Domain: "", // Must be empty
|
||||
Path: "/", // Required
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gorilla Sessions Cookie Security — High
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "github.com/gorilla/sessions"
|
||||
store := sessions.NewCookieStore([]byte("secret-key")) // DON'T: Hardcoded key
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "github.com/gorilla/sessions"
|
||||
|
||||
store := sessions.NewCookieStore(
|
||||
[]byte(os.Getenv("SESSION_AUTH_KEY")), // Use env var
|
||||
[]byte(os.Getenv("SESSION_ENC_KEY")), // Separate encryption key
|
||||
)
|
||||
|
||||
store.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cookie Best Practices Checklist
|
||||
|
||||
- [ ] Set `HttpOnly: true` for all authentication cookies
|
||||
- [ ] Set `Secure: true` for all cookies over HTTPS
|
||||
- [ ] Set appropriate `SameSite` value (Strict/Lax/None)
|
||||
- [ ] Use short `MaxAge` expiration
|
||||
- [ ] Avoid setting cookie `Domain` unless necessary
|
||||
- [ ] Validate cookie values on every request
|
||||
- [ ] Use cryptographically signed cookies
|
||||
- [ ] Rotate cookie secrets regularly
|
||||
- [ ] Clear cookies on logout
|
||||
- [ ] Use double-submit cookie pattern for CSRF protection
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-1004**: Sensitive Cookie Without 'HttpOnly' Flag
|
||||
- **CWE-614**: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute
|
||||
- **CWE-352**: Cross-Site Request Forgery (CSRF)
|
||||
- **CWE-285**: Improper Authorization
|
||||
- **CWE-565**: Reliance on Cookies without Validation
|
||||
@@ -0,0 +1,424 @@
|
||||
# Cryptography Security Rules
|
||||
|
||||
Cryptography vulnerabilities threaten confidentiality and integrity of sensitive data.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. TLS MUST use 1.2+.
|
||||
2. NEVER use DES, RC4, MD5, or SHA1 for security purposes.
|
||||
3. SSH host keys MUST be verified — NEVER use `InsecureIgnoreHostKey`.
|
||||
4. Passwords MUST be hashed with Argon2id (preferred) or bcrypt.
|
||||
5. Security-critical randomness MUST use `crypto/rand`.
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Selection Guide
|
||||
|
||||
Choose the right algorithm for the job — using the wrong primitive (e.g. SHA256 for passwords) is as dangerous as using a broken one:
|
||||
|
||||
| Use Case | Recommended | Avoid | Why |
|
||||
| --- | --- | --- | --- |
|
||||
| Symmetric encryption | AES-256-GCM, ChaCha20-Poly1305 | DES, 3DES, AES-ECB, RC4 | ECB reveals patterns; DES/RC4 are broken |
|
||||
| Password hashing | Argon2id (preferred), bcrypt, scrypt | MD5, SHA-1, plain SHA-256 | Fast hashes enable brute-force; memory-hard functions resist GPU attacks |
|
||||
| Message authentication | HMAC-SHA256, Poly1305 | HMAC-MD5, HMAC-SHA1 | MD5/SHA1 have known collision weaknesses |
|
||||
| Digital signatures | Ed25519, ECDSA P-256 | RSA-PKCS1v1.5 | PKCS1v1.5 has padding oracle vulnerabilities |
|
||||
| Key exchange | X25519, ECDH P-256 | Static RSA key transport | Forward secrecy requires ephemeral keys |
|
||||
| Random generation | `crypto/rand` | `math/rand` | `math/rand` output is predictable |
|
||||
| TLS | TLS 1.2+ (prefer 1.3) | TLS 1.0, 1.1, SSL | Known attacks (BEAST, POODLE) on older versions |
|
||||
|
||||
### Key Size Requirements
|
||||
|
||||
| Algorithm | Minimum Key Size | Recommended |
|
||||
| --------- | ------------------------ | ---------------- |
|
||||
| RSA | 2048 bits | 4096 bits |
|
||||
| AES | 128 bits | 256 bits |
|
||||
| ECDSA | P-256 (128-bit security) | P-256 or Ed25519 |
|
||||
|
||||
---
|
||||
|
||||
## Key Rotation Pattern
|
||||
|
||||
Keys should be rotated periodically. Use envelope encryption so rotating the Key Encryption Key (KEK) doesn't require re-encrypting all data:
|
||||
|
||||
```go
|
||||
// Envelope encryption: encrypt data with a DEK, encrypt DEK with KEK
|
||||
func EnvelopeEncrypt(kek, plaintext []byte) (encryptedDEK, ciphertext []byte, err error) {
|
||||
// 1. Generate random Data Encryption Key
|
||||
dek := make([]byte, 32)
|
||||
if _, err := rand.Read(dek); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 2. Encrypt data with DEK
|
||||
ciphertext, err = EncryptAESGCM(dek, plaintext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 3. Encrypt DEK with KEK
|
||||
encryptedDEK, err = EncryptAESGCM(kek, dek)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return encryptedDEK, ciphertext, nil
|
||||
}
|
||||
|
||||
func EnvelopeDecrypt(kek, encryptedDEK, ciphertext []byte) ([]byte, error) {
|
||||
dek, err := DecryptAESGCM(kek, encryptedDEK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DecryptAESGCM(dek, ciphertext)
|
||||
}
|
||||
|
||||
func EncryptAESGCM(key, plaintext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil { return nil, err }
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil { return nil, err }
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil { return nil, err }
|
||||
return aead.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func DecryptAESGCM(key, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil { return nil, err }
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil { return nil, err }
|
||||
nonceSize := aead.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
return aead.Open(nil, ciphertext[:nonceSize], ciphertext[nonceSize:], nil)
|
||||
}
|
||||
```
|
||||
|
||||
When the KEK is rotated, only re-encrypt the DEKs (small), not the data (potentially large).
|
||||
|
||||
---
|
||||
|
||||
## Common Cryptographic Mistakes
|
||||
|
||||
### Mistake 1: AES-ECB reveals patterns — High
|
||||
|
||||
ECB encrypts each block independently — identical plaintext blocks produce identical ciphertext blocks, revealing data structure:
|
||||
|
||||
```go
|
||||
// Bad — ECB mode reveals patterns in structured data
|
||||
block, _ := aes.NewCipher(key)
|
||||
// Using block.Encrypt directly = ECB mode
|
||||
|
||||
// Good — GCM provides authenticated encryption
|
||||
aead, err := cipher.NewGCM(block) // randomized, authenticated
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
|
||||
```
|
||||
|
||||
### Mistake 2: Reusing nonces — Critical
|
||||
|
||||
A nonce reuse with AES-GCM completely breaks confidentiality and authentication:
|
||||
|
||||
```go
|
||||
// Bad — static or reused nonce
|
||||
nonce := []byte("fixed_nonce!") // catastrophic with GCM
|
||||
|
||||
// Good — random nonce per encryption
|
||||
nonce := make([]byte, 12) // 96-bit for GCM
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 3: Non-constant-time comparison for secrets — Medium
|
||||
|
||||
Comparing secrets with `==` short-circuits on the first differing byte, leaking timing information. See [Network/Web Security — Observable Timing](./network.md) for constant-time comparison patterns using `crypto/subtle`.
|
||||
|
||||
---
|
||||
|
||||
## Insecure TLS Configuration — High
|
||||
|
||||
Using insecure TLS configurations can expose your application to man-in-the-middle attacks.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // DON'T: verify certificates
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/tls"
|
||||
|
||||
func secureConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DES Encryption — High
|
||||
|
||||
DES is cryptographically broken.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/des"
|
||||
block, _ := des.NewCipher(key) // DON'T: broken
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/aes"
|
||||
block, _ := aes.NewCipher(key) // OK: AES
|
||||
cipher.NewGCM(block) // OK: GCM for auth
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure SSH Host Key Verification — High
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "golang.org/x/crypto/ssh"
|
||||
&ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // DON'T
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "golang.org/x/crypto/ssh"
|
||||
&ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.FixedHostKey(publicKey),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MD5 Hash — High
|
||||
|
||||
MD5 is collision-prone and weak for security.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/md5"
|
||||
hash := md5.Sum([]byte(data)) // DON'T: weak
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// For password hashing:
|
||||
import "golang.org/x/crypto/argon2"
|
||||
hash := argon2.IDKey([]byte(pw), salt, 3, 64*1024, 4, 32)
|
||||
|
||||
// Or bcrypt (simpler API, no salt management):
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For general-purpose hashing (not passwords):
|
||||
import "crypto/sha256"
|
||||
digest := sha256.Sum256(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RC4 Cipher — High
|
||||
|
||||
RC4 is cryptographically broken.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/rc4"
|
||||
cipher, _ := rc4.NewCipher(key) // DON'T: broken
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/cipher"
|
||||
import "crypto/aes"
|
||||
aead, _ := cipher.NewGCM(block) // OK: AES-GCM
|
||||
|
||||
// Or ChaCha20:
|
||||
import "golang.org/x/crypto/chacha20poly1305"
|
||||
aead, _ := chacha20poly1305.New(key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SHA1 Hash — Medium
|
||||
|
||||
SHA1 provides insufficient collision resistance.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/sha1"
|
||||
hash := sha1.Sum(data) // DON'T: weak
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/sha256"
|
||||
hash := sha256.Sum256(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weak Cryptographic Algorithms — Medium
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/hmac"
|
||||
import "crypto/md5"
|
||||
mac := hmac.New(md5.New, key) // DON'T: HMAC-MD5
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/sha256"
|
||||
mac := hmac.New(sha256.New, key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insufficient Key Strength — Medium
|
||||
|
||||
RSA keys smaller than 2048 bits are insufficient.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/rsa"
|
||||
key, _ := rsa.GenerateKey(rand.Reader, 1024) // DON'T: too weak
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
key, _ := rsa.GenerateKey(rand.Reader, 4096) // OK: 2048+ bits
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weak Random Number Generators — High
|
||||
|
||||
`math/rand` is predictable, never use for security.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "math/rand"
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes) // DON'T: predictable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/rand"
|
||||
_, err := rand.Read(bytes) // OK: cryptographically secure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weak TLS Versions — High
|
||||
|
||||
TLS 1.0 and 1.1 have known vulnerabilities.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/tls"
|
||||
&tls.Config{MinVersion: tls.VersionTLS10} // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
&tls.Config{MinVersion: tls.VersionTLS12} // OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Password Hashing — High
|
||||
|
||||
Don't use MD5, SHA1, or single-iteration hashes for passwords.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "crypto/sha256"
|
||||
hash := sha256.Sum256([]byte(password)) // DON'T: too fast
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Argon2id (preferred) — memory-hard, resists GPU attacks:
|
||||
import "golang.org/x/crypto/argon2"
|
||||
key := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
|
||||
|
||||
// Or bcrypt (simpler API, widely supported):
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Or PBKDF2 with 600,000+ iterations (Go 1.24+ stdlib):
|
||||
import "crypto/pbkdf2"
|
||||
key, err := pbkdf2.Key(sha512.New, password, salt, 600_000, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Or scrypt:
|
||||
import "golang.org/x/crypto/scrypt"
|
||||
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
For Go 1.24+, prefer stdlib `crypto/hkdf`, `crypto/pbkdf2`, and `crypto/sha3`. Use `golang.org/x/crypto/...` fallbacks only for modules targeting older Go versions or for algorithms still outside the standard library.
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-327**: Use of a Broken or Risky Cryptographic Algorithm
|
||||
- **CWE-331**: Insufficient Entropy
|
||||
- **CWE-326**: Inadequate Encryption Strength
|
||||
- **CWE-295**: Improper Certificate Validation
|
||||
- **CWE-330**: Use of Insufficiently Random Values
|
||||
- **CWE-916**: Use of Password Hash With Insufficient Computational Effort
|
||||
@@ -0,0 +1,285 @@
|
||||
# Filesystem Security Rules
|
||||
|
||||
Filesystem vulnerabilities can lead to unauthorized file access, data leakage, and denial-of-service attacks.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. User-controlled file paths MUST be confined to an allowed root.
|
||||
2. `os.Root` SHOULD be used for scoped file access (Go 1.24+).
|
||||
3. Zip extraction MUST check for ZipSlip path traversal.
|
||||
4. Temporary files MUST use `os.CreateTemp` — NEVER predictable names.
|
||||
5. File permissions MUST be restrictive (0600 for secrets, 0750 for directories).
|
||||
|
||||
---
|
||||
|
||||
## Directory Traversal — High
|
||||
|
||||
Paths like `../../etc/passwd` access files outside intended directory.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
filepath := filepath.Join("/var/www", filename) // DON'T
|
||||
http.ServeFile(w, r, filepath)
|
||||
```
|
||||
|
||||
**Good (Go 1.24+) — use `os.Root` for safe, scoped directory access:**
|
||||
|
||||
```go
|
||||
root, err := os.OpenRoot("/var/www")
|
||||
if err != nil { return err }
|
||||
defer root.Close()
|
||||
f, err := root.Open(filename) // cannot escape root directory
|
||||
```
|
||||
|
||||
`os.Root` prevents ordinary path traversal at the OS level. All operations (`Open`, `Create`, `Stat`, `OpenFile`, etc.) are confined to the root directory, and symlinks that resolve outside the root are rejected. It is not a full sandbox: it does not by itself block bind mounts, special device files, or all `/proc`-style filesystem behavior. For archive extraction and uploads, still reject special files and choose a root without attacker-controlled mounts.
|
||||
|
||||
**Good (pre-Go 1.24 fallback):**
|
||||
|
||||
```go
|
||||
func safeJoin(baseDir, userPath string) (string, error) {
|
||||
if userPath == "" || filepath.IsAbs(userPath) || !filepath.IsLocal(userPath) {
|
||||
return "", errors.New("invalid relative path")
|
||||
}
|
||||
|
||||
full := filepath.Join(baseDir, userPath)
|
||||
|
||||
rel, err := filepath.Rel(baseDir, full)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking path: %w", err)
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||
return "", errors.New("path escapes base directory")
|
||||
}
|
||||
|
||||
return full, nil
|
||||
}
|
||||
```
|
||||
|
||||
This lexical fallback is not a full symlink-resistant substitute for `os.Root`.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
fullPath := filepath.Join(baseDir, filename)
|
||||
if !strings.HasPrefix(filepath.Clean(fullPath), filepath.Clean(baseDir)) {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zip Archive Path Traversal — High
|
||||
|
||||
Malicious zip files can escape extraction directory.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
for _, file := range reader.File {
|
||||
path := filepath.Join(dest, file.Name) // DON'T: No validation
|
||||
file.Create(path)
|
||||
}
|
||||
```
|
||||
|
||||
**Good (Go 1.24+) — use `os.Root` to scope extraction:**
|
||||
|
||||
```go
|
||||
root, err := os.OpenRoot(dest)
|
||||
if err != nil { return err }
|
||||
defer root.Close()
|
||||
for _, file := range reader.File {
|
||||
f, err := root.OpenFile(file.Name, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil { return err } // rejects paths escaping root
|
||||
// ... copy contents ...
|
||||
f.Close()
|
||||
}
|
||||
```
|
||||
|
||||
**Good (pre-Go 1.24 fallback):**
|
||||
|
||||
```go
|
||||
for _, file := range reader.File {
|
||||
if !filepath.IsLocal(file.Name) {
|
||||
return fmt.Errorf("unsafe archive path: %q", file.Name)
|
||||
}
|
||||
|
||||
targetPath, err := safeJoin(dest, file.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create parent directories, then write targetPath
|
||||
_ = targetPath
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decompression Bomb — Medium
|
||||
|
||||
Tiny compressed files can expand to GBs.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
gr, _ := gzip.NewReader(f)
|
||||
out, _ := os.Create(dst)
|
||||
io.Copy(out, gr) // DON'T: No size limits
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
const maxDecompressedSize = 100 * 1024 * 1024 // 100MB limit
|
||||
|
||||
var errDecompressedSizeLimitExceeded = errors.New("decompressed size limit exceeded")
|
||||
|
||||
type limitedReader struct {
|
||||
r io.Reader
|
||||
read int64
|
||||
}
|
||||
|
||||
func (l *limitedReader) Read(p []byte) (int, error) {
|
||||
if l.read >= maxDecompressedSize {
|
||||
// Return a sentinel error — io.EOF would be treated as success by io.Copy
|
||||
return 0, errDecompressedSizeLimitExceeded
|
||||
}
|
||||
n, err := l.r.Read(p)
|
||||
l.read += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
lr := &limitedReader{r: gr}
|
||||
if _, err := io.Copy(out, lr); err != nil {
|
||||
return fmt.Errorf("decompressing: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure Temporary File Creation — Medium
|
||||
|
||||
Creating temp files without proper permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
f, _ := os.Create("/tmp/myapp.temp") // DON'T: Predictable name
|
||||
f.WriteString(data)
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
f, err := os.CreateTemp("", "myapp.*")
|
||||
defer os.Remove(f.Name())
|
||||
f.Chmod(0600) // Restrictive permissions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure File Permissions — Medium
|
||||
|
||||
Opening files with excessive permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
f, _ := os.OpenFile("config.json", os.O_CREATE, 0644) // DON'T: World-readable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
f, _ := os.OpenFile("config.json", os.O_CREATE, 0600) // OK: Owner only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure mkdir — Low
|
||||
|
||||
Creating directories with overly permissive permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
os.MkdirAll("/var/myapp/cache", 0777) // DON'T: World-writable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
os.MkdirAll("/var/myapp/cache", 0750) // OK: Group-writable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure File Write Permissions — Medium
|
||||
|
||||
Opening files for writing with inappropriate permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
os.OpenFile("app.log", os.O_CREATE, 0666) // DON'T: World-writable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
os.OpenFile("app.log", os.O_CREATE|os.O_APPEND, 0640) // OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tainted File Read — High
|
||||
|
||||
Reading files based on unvalidated input.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
return os.ReadFile(filename) // DON'T: No validation
|
||||
}
|
||||
```
|
||||
|
||||
**Good (Go 1.24+):**
|
||||
|
||||
```go
|
||||
const allowedDir = "/var/www/public/"
|
||||
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
root, err := os.OpenRoot(allowedDir)
|
||||
if err != nil { return nil, err }
|
||||
defer root.Close()
|
||||
f, err := root.Open(filename) // cannot escape root directory
|
||||
if err != nil { return nil, err }
|
||||
defer f.Close()
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
```
|
||||
|
||||
**Good (pre-Go 1.24 fallback):**
|
||||
|
||||
```go
|
||||
const allowedDir = "/var/www/public/"
|
||||
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
fullPath, err := safeJoin(allowedDir, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(fullPath)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-22**: Path Traversal (Directory Traversal)
|
||||
- **CWE-409**: Zip Bomb Decompression
|
||||
- **CWE-379**: Insecure Temp File Creation
|
||||
- **CWE-732**: Incorrect File Permissions
|
||||
@@ -0,0 +1,315 @@
|
||||
# Injection Security Rules
|
||||
|
||||
Injection vulnerabilities allow attackers to execute arbitrary code, queries, or commands.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. SQL queries MUST use parameterized placeholders — NEVER concatenate user input.
|
||||
2. Command execution MUST use `exec.Command` with separate args — NEVER shell interpolation.
|
||||
3. HTML output MUST use `html/template` for automatic escaping.
|
||||
4. SSRF: outbound URLs MUST be validated against an allowlist.
|
||||
|
||||
---
|
||||
|
||||
## SQL Injection — Critical
|
||||
|
||||
Building SQL queries by concatenating user input. Always use prepared statements with placeholders.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", input)
|
||||
query := "SELECT * FROM users WHERE id = " + id
|
||||
query := "DELETE FROM orders WHERE id = " + strconv.Itoa(orderID) // safe but inconsistent — use placeholders everywhere
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Placeholder syntax varies by driver: $1 (pgx/lib/pq), ? (MySQL/SQLite)
|
||||
db.QueryRow("SELECT * FROM users WHERE name = $1", input)
|
||||
db.Exec("DELETE FROM orders WHERE id = $1", orderID)
|
||||
```
|
||||
|
||||
### Dynamic IN clauses
|
||||
|
||||
Never build `IN (...)` by joining user strings. Generate numbered placeholders.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
query := fmt.Sprintf("SELECT * FROM users WHERE id IN (%s)", strings.Join(ids, ","))
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Build placeholders: $1, $2, $3, ...
|
||||
placeholders := make([]string, len(ids))
|
||||
args := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||
args[i] = id
|
||||
}
|
||||
query := fmt.Sprintf("SELECT * FROM users WHERE id IN (%s)", strings.Join(placeholders, ","))
|
||||
rows, err := db.Query(query, args...)
|
||||
```
|
||||
|
||||
With `sqlx`:
|
||||
|
||||
```go
|
||||
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
|
||||
query = db.Rebind(query) // converts ? to $1,$2,... for postgres
|
||||
rows, err := db.Query(query, args...)
|
||||
```
|
||||
|
||||
### Dynamic column names and ORDER BY
|
||||
|
||||
Placeholders only work for **values**, not identifiers (table/column names) or SQL keywords. Allowlist identifiers explicitly.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", sortCol) // SQL injection
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
allowed := map[string]string{
|
||||
"name": "name", "created": "created_at", "email": "email",
|
||||
}
|
||||
col, ok := allowed[sortCol]
|
||||
if !ok {
|
||||
col = "created_at"
|
||||
}
|
||||
query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", col) // safe: col is from allowlist
|
||||
```
|
||||
|
||||
### Dynamic WHERE filters
|
||||
|
||||
Build queries incrementally; parameterize every user-supplied value.
|
||||
|
||||
```go
|
||||
var conditions []string
|
||||
var args []any
|
||||
idx := 1
|
||||
|
||||
if name != "" {
|
||||
conditions = append(conditions, fmt.Sprintf("name = $%d", idx))
|
||||
args = append(args, name)
|
||||
idx++
|
||||
}
|
||||
if minAge > 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("age >= $%d", idx))
|
||||
args = append(args, minAge)
|
||||
idx++
|
||||
}
|
||||
|
||||
query := "SELECT * FROM users"
|
||||
if len(conditions) > 0 {
|
||||
query += " WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
rows, err := db.Query(query, args...)
|
||||
```
|
||||
|
||||
### Prefer `sqlx` or `pgx` over raw `database/sql`
|
||||
|
||||
Libraries like `sqlx` and `pgx` provide safer ergonomics (named parameters, `IN` clause expansion, struct scanning) while still using prepared statements under the hood. They reduce the temptation to fall back to string concatenation for complex queries.
|
||||
|
||||
---
|
||||
|
||||
## XPath Injection — High
|
||||
|
||||
XPath injection allows manipulation of XML data queries.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
xpathQuery := "//user[@username='" + username + "']" // Vulnerable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Use numeric ID
|
||||
xpathQuery := fmt.Sprintf("//user[@id='%d']", userID)
|
||||
|
||||
// Or parse XML without XPath
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Injection — Critical
|
||||
|
||||
Generating code from unvalidated user input.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
template := "func handle" + resourceName + "() {...}" // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Validate resource name matches whitelist
|
||||
if !allowedResources[resourceName] {
|
||||
return errors.New("invalid resource")
|
||||
}
|
||||
// Use predefined templates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Injection — Critical
|
||||
|
||||
Passing unvalidated input to shell commands.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
cmd := exec.Command("sh", "-c", "rm -f /tmp/"+filename) // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
cmd := exec.Command("rm", "-f", filepath.Join("/tmp", filename))
|
||||
|
||||
// Better: validate filename
|
||||
if filepath.Base(filename) != filename {
|
||||
return errors.New("invalid filename")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Injection — High
|
||||
|
||||
Using untrusted input in templates.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
data := r.URL.Query().Get("user") // Untrusted input
|
||||
t.Execute(w, data)
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Validate input
|
||||
user := strings.TrimSpace(r.URL.Query().Get("user"))
|
||||
if !allowedRoles[role] {
|
||||
role = "user"
|
||||
}
|
||||
t.Execute(w, data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Site Scripting (XSS) — High
|
||||
|
||||
XSS allows attackers to execute malicious scripts.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
w.Write([]byte(fmt.Sprintf("<div>%s</div>", data))) // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "html/template"
|
||||
t := template.Must(template.New("safe").Parse("<div>{{.}}</div>"))
|
||||
t.Execute(w, data) // Auto-escapes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTML Tag Injection — High
|
||||
|
||||
Injecting HTML tags through unvalidated input.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
fmt.Fprintf(w, "<div>Welcome, %s!</div>", input) // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "html"
|
||||
escaped := html.EscapeString(input)
|
||||
fmt.Fprintf(w, "<div>Welcome, %s!</div>", escaped)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server-Side Request Forgery (SSRF) — High
|
||||
|
||||
Forcing the server to make requests to unintended endpoints.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
url := r.URL.Query().Get("url")
|
||||
resp, _ := http.Get(url) // DON'T: No validation
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
u, err := url.Parse(targetURL)
|
||||
// Block non-HTTP/S protocols
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return errors.New("invalid scheme")
|
||||
}
|
||||
// Block internal hosts
|
||||
if isInternalIP(u.Hostname()) {
|
||||
return errors.New("internal host not allowed")
|
||||
}
|
||||
// Block metadata endpoints
|
||||
if strings.Contains(u.Hostname(), "metadata.") {
|
||||
return errors.New("metadata endpoint blocked")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unsafe Deserialization — Critical
|
||||
|
||||
Deserializing untrusted input can lead to resource exhaustion, type confusion, or unsafe object construction.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
dec := gob.NewDecoder(r.Body) // DON'T: gob is not hardened for adversarial input
|
||||
var user interface{}
|
||||
dec.Decode(&user)
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "encoding/json"
|
||||
dec := json.NewDecoder(r.Body)
|
||||
var user User
|
||||
dec.Decode(&user) // JSON doesn't execute code
|
||||
// Validate fields
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-78**: OS Command Injection
|
||||
- **CWE-89**: SQL Injection
|
||||
- **CWE-94**: Code Injection
|
||||
- **CWE-79**: Cross-site Scripting (XSS)
|
||||
- **CWE-918**: Server-Side Request Forgery (SSRF)
|
||||
- **CWE-502**: Deserialization of Untrusted Data
|
||||
- **CWE-20**: Improper Input Validation
|
||||
@@ -0,0 +1,163 @@
|
||||
# Logging Security Rules
|
||||
|
||||
Logging sensitive information can lead to data exposure and compliance violations.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. PII MUST NEVER be logged — filter passwords, tokens, emails, and personal data.
|
||||
2. Log injection MUST be prevented — sanitize user input before logging.
|
||||
3. Error messages MUST NOT expose internals to users — log details server-side, return generic messages.
|
||||
|
||||
---
|
||||
|
||||
## Sensitive Data in Logs — Medium
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
Password string
|
||||
Token string
|
||||
}
|
||||
|
||||
func logUserLogin(user *User) {
|
||||
log.Printf("User logged in: %+v\n", user) // DON'T: Logs password, token
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "log/slog"
|
||||
|
||||
func logUserLogin(logger *slog.Logger, user *User) {
|
||||
logger.Info("user_login",
|
||||
"user_id", user.ID,
|
||||
"username", user.Username,
|
||||
// Don't log: password, token
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Injection — Low
|
||||
|
||||
User input in logs can lead to log injection attacks.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
log.Printf("User logged in: %s\n", username) // DON'T: No sanitization
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "log/slog"
|
||||
|
||||
// Sanitize user input before logging
|
||||
func sanitizeLogInput(input string) string {
|
||||
// Remove control characters
|
||||
var result strings.Builder
|
||||
for _, r := range input {
|
||||
if !unicode.IsControl(r) || r == '\n' || r == '\t' {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func logUsername(logger *slog.Logger, username string) {
|
||||
sanitized := sanitizeLogInput(username)
|
||||
logger.Info("user_login", "username", sanitized)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Information Leakage in Error Messages — Medium
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func handleDatabaseError(err error) error {
|
||||
return fmt.Errorf("database error: %v", err) // DON'T: Leaks internal details
|
||||
}
|
||||
|
||||
func dbErrorToHTTP(err error) {
|
||||
http.Error(w, "Error: "+err.Error(), 500) // DON'T
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
func handleDatabaseError(logger *slog.Logger, err error) error {
|
||||
// Log detailed error for debugging
|
||||
logger.Error("database_error", "error", err.Error())
|
||||
// Return generic message to client
|
||||
return errors.New("database operation failed")
|
||||
}
|
||||
|
||||
func dbErrorToHTTP(w http.ResponseWriter, logger *slog.Logger, err error) {
|
||||
logger.Error("database_error", "error", err.Error())
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## General Logger Security — Low
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "log"
|
||||
log.Println("User logged in:", user.ID, password) // DON'T: Logs password
|
||||
fmt.Printf("DEBUG: %+v\n", data) // DON'T: Raw data
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "log/slog"
|
||||
|
||||
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
AddSource: false,
|
||||
})
|
||||
logger := slog.New(handler)
|
||||
|
||||
logger.Info("user_login",
|
||||
"user_id", userID,
|
||||
"ip_address", request.RemoteIP,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Security Checklist
|
||||
|
||||
- [ ] No passwords, tokens, or secrets in logs
|
||||
- [ ] No PII/PHI in production logs
|
||||
- [ ] Sanitize user input before logging
|
||||
- [ ] Use structured logging (JSON)
|
||||
- [ ] Implement log level strategy
|
||||
- [ ] Separate access logs from error logs
|
||||
- [ ] Log file permissions restricted (e.g., 600)
|
||||
- [ ] Log rotation prevents disk exhaustion
|
||||
- [ ] Generic error messages to clients
|
||||
- [ ] Detailed errors only in internal logs
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-532**: Insertion of Sensitive Information into Log File
|
||||
- **CWE-117**: Improper Output Neutralization for Logs
|
||||
- **CWE-209**: Information Exposure Through an Error Message
|
||||
- **CWE-200**: Exposure of Sensitive Information
|
||||
- **CWE-312**: Cleartext Storage of Sensitive Information
|
||||
@@ -0,0 +1,241 @@
|
||||
# Memory Safety Security Rules
|
||||
|
||||
Memory safety vulnerabilities can lead to crashes, data corruption, and security compromises.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. Integer overflow MUST be checked at boundaries — NEVER trust unchecked arithmetic on external input.
|
||||
2. `unsafe` MUST NOT be used in application code — restrict to low-level libraries with thorough review.
|
||||
3. Data races MUST be detected with `-race` flag in CI.
|
||||
|
||||
---
|
||||
|
||||
## Integer Overflow — High
|
||||
|
||||
Integer overflows can cause unexpected behavior and crashes.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func allocateBuffer(rows, cols int) []byte {
|
||||
size := rows * cols // DON'T: Can overflow
|
||||
return make([]byte, size)
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "math"
|
||||
|
||||
func safeMultiply(a, b int) (int, error) {
|
||||
if a == 0 || b == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if a > math.MaxInt/b {
|
||||
return 0, errors.New("integer overflow")
|
||||
}
|
||||
result := a * b
|
||||
if result/b != a {
|
||||
return 0, errors.New("overflow detected")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func allocateBuffer(rows, cols int) ([]byte, error) {
|
||||
size, err := safeMultiply(rows, cols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const maxBufferSize = 100 * 1024 * 1024 // 100MB limit
|
||||
if size > maxBufferSize {
|
||||
return nil, errors.New("buffer size exceeds limit")
|
||||
}
|
||||
return make([]byte, size), nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## math/big.Rat Issues — Low
|
||||
|
||||
Rat can consume large amounts of memory if denominators grow without bounds.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "math/big"
|
||||
|
||||
func unsafeFraction(operations int) *big.Rat {
|
||||
r := big.NewRat(1, 1)
|
||||
for i := 0; i < operations; i++ {
|
||||
r.Mul(r, big.NewRat(int64(i+1), int64(i+2))) // DON'T
|
||||
}
|
||||
return r // Could be memory intensive
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
const maxRatNumBits = 1000
|
||||
|
||||
func safeFraction(operations int) (*big.Rat, error) {
|
||||
r := big.NewRat(1, 1)
|
||||
for i := 0; i < operations; i++ {
|
||||
r.Mul(r, big.NewRat(int64(i+1), int64(i+2)))
|
||||
if r.Num().BitLen() > maxRatNumBits || r.Denom().BitLen() > maxRatNumBits {
|
||||
return nil, errors.New("fraction precision too large")
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Aliasing Vulnerability — Medium
|
||||
|
||||
Memory aliasing can cause data corruption and race conditions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func reverseBytes(data []byte) {
|
||||
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
|
||||
data[i], data[j] = data[j], data[i] // DON'T if slices alias
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "unsafe"
|
||||
|
||||
func checkOverlap(a, b []byte) bool {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
aStart := uintptr(unsafe.Pointer(&a[0]))
|
||||
aEnd := aStart + uintptr(len(a))
|
||||
bStart := uintptr(unsafe.Pointer(&b[0]))
|
||||
bEnd := bStart + uintptr(len(b))
|
||||
return aStart < bEnd && bStart < aEnd
|
||||
}
|
||||
|
||||
func safeCopy(dest, src []byte) {
|
||||
if checkOverlap(dest, src) {
|
||||
temp := make([]byte, len(src))
|
||||
copy(temp, src)
|
||||
copy(dest, temp)
|
||||
} else {
|
||||
copy(dest, src)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use of unsafe Package — High
|
||||
|
||||
The unsafe package bypasses Go's type safety and memory safety.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "unsafe"
|
||||
|
||||
func UnsafeStringToBytes(s string) []byte {
|
||||
return (*[0x7fffffff]byte)(unsafe.Pointer(
|
||||
(*reflect.StringHeader)(unsafe.Pointer(&s)).Data,
|
||||
))[:len(s):len(s)] // DON'T: memory corruption risk
|
||||
}
|
||||
|
||||
func TypePun(value uint64) float64 {
|
||||
return *(*float64)(unsafe.Pointer(&value)) // DON'T
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Safe string encoding
|
||||
func StringToBytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
func BytesToString(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Safe type conversion
|
||||
import "encoding/binary"
|
||||
func Uint64ToFloat64(value uint64) float64 {
|
||||
buf := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(buf, value)
|
||||
bits := binary.LittleEndian.Uint64(buf)
|
||||
return math.Float64frombits(bits)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Races — High
|
||||
|
||||
Go's race detector is your primary defense.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
type Counter struct {
|
||||
value int
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.value++ // DON'T: Data race without sync
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "sync"
|
||||
|
||||
type Counter struct {
|
||||
value int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.value++
|
||||
}
|
||||
|
||||
// Or atomic for simple cases
|
||||
import "sync/atomic"
|
||||
type AtomicCounter struct {
|
||||
value int64
|
||||
}
|
||||
func (c *AtomicCounter) Increment() {
|
||||
atomic.AddInt64(&c.value, 1)
|
||||
}
|
||||
```
|
||||
|
||||
## Always Run Race Detector
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
go build -race
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-190**: Integer Overflow or Wraparound
|
||||
- **CWE-119**: Improper Restriction of Operations within Bounds
|
||||
- **CWE-125**: Out-of-bounds Read
|
||||
- **CWE-787**: Out-of-bounds Write
|
||||
- **CWE-362**: Race Condition
|
||||
- **CWE-367**: Time-of-check Time-of-use (TOCTOU)
|
||||
@@ -0,0 +1,253 @@
|
||||
# Network/Web Security Rules
|
||||
|
||||
Network and web security vulnerabilities can lead to data leakage and unauthorized access.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. Redirects MUST be validated against an allowlist of domains.
|
||||
2. HTTP servers MUST configure `ReadTimeout`, `WriteTimeout`, and `IdleTimeout`.
|
||||
3. Pprof endpoints MUST NEVER be exposed publicly.
|
||||
4. XML parsers MUST disable XXE — reject `<!DOCTYPE` and `<!ENTITY` declarations.
|
||||
|
||||
---
|
||||
|
||||
## Open Redirect Vulnerability — Medium
|
||||
|
||||
Redirects to unvalidated URLs can be used for phishing.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
target := r.URL.Query().Get("url")
|
||||
http.Redirect(w, r, target, http.StatusFound) // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
target := r.URL.Query().Get("url")
|
||||
u, _ := url.Parse(target)
|
||||
// Only allow http/https
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return errors.New("invalid scheme")
|
||||
}
|
||||
// Block javascript/data schemes
|
||||
if strings.HasPrefix(target, "javascript:") || strings.HasPrefix(target, "data:") {
|
||||
return errors.New("blocked scheme")
|
||||
}
|
||||
// Check against whitelist
|
||||
if !isAllowedDomain(u.Host) {
|
||||
return errors.New("invalid domain")
|
||||
}
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bind to All Interfaces — Medium
|
||||
|
||||
Binding to 0.0.0.0 exposes services to all network interfaces.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
listener, _ := net.Listen("tcp", "0.0.0.0:8080") // DON'T: Exposes all interfaces
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Bind only to localhost
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:8080")
|
||||
|
||||
// Or specific internal IP
|
||||
listener, _ := net.Listen("tcp", "10.0.1.5:8080")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slowloris Attack Vulnerability — Medium
|
||||
|
||||
Slowloris attacks exhaust connection pools.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
// Missing ReadTimeout, WriteTimeout, IdleTimeout
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // Limit header size to 1MB
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure HTTP Server Configuration — Medium
|
||||
|
||||
Running HTTP servers without proper security settings.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
http.ListenAndServe(":8080", handler) // DON'T: No security hardening
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
server := &http.Server{
|
||||
Addr: ":443",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
server.ListenAndServeTLS("cert.pem", "key.pem")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Observable Timing (Timing Attacks) — Medium
|
||||
|
||||
Timing differences can leak sensitive information.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func checkPassword(input, secret string) bool {
|
||||
return input == secret // DON'T: Short-circuit leaks length
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "crypto/subtle"
|
||||
|
||||
// For comparing fixed-length tokens or hashes:
|
||||
func checkToken(input, expected string) bool {
|
||||
// ConstantTimeCompare already handles unequal lengths without leaking timing
|
||||
return subtle.ConstantTimeCompare([]byte(input), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
// For passwords, use Argon2id (preferred) or bcrypt — they handle hashing
|
||||
// and constant-time comparison internally:
|
||||
// argon2: hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
|
||||
// bcrypt: err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
||||
|
||||
// For HMAC verification:
|
||||
import "crypto/hmac"
|
||||
func verifyHMAC(message, messageMAC, key []byte) bool {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(message)
|
||||
expectedMAC := mac.Sum(nil)
|
||||
return hmac.Equal(messageMAC, expectedMAC) // Constant-time
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exposed pprof Profiling Endpoints — High
|
||||
|
||||
Debug pprof endpoints expose sensitive runtime information.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import _ "net/http/pprof" // DON'T: Automatically registers /debug/pprof
|
||||
http.ListenAndServe(":8080", handler)
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// Option 1: Use build tags to exclude pprof from production builds
|
||||
// File: debug_pprof.go
|
||||
//go:build !production
|
||||
|
||||
package main
|
||||
|
||||
import _ "net/http/pprof"
|
||||
|
||||
// Option 2: Serve pprof on a separate internal-only listener
|
||||
func startDebugServer() {
|
||||
debugMux := http.NewServeMux()
|
||||
debugMux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
debugMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
debugMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
debugMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
debugMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
go http.ListenAndServe("127.0.0.1:6060", debugMux) // localhost only
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## XXE Vulnerability — High
|
||||
|
||||
XML parsers that process external entity references.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||
decoder.Decode(&person) // DON'T: May process external entities
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||
decoder.Strict = true
|
||||
|
||||
// Block DTD declarations
|
||||
xmlStr := string(xmlData)
|
||||
if strings.Contains(xmlStr, "<!DOCTYPE") || strings.Contains(xmlStr, "<!ENTITY") {
|
||||
return errors.New("XML contains DTD - potential XXE")
|
||||
}
|
||||
decoder.Decode(&person)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permissive Regex Validation — Low
|
||||
|
||||
Weak regex validation can allow malicious input.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
matched, _ := regexp.MatchString(`.+@.+\..+`, email) // DON'T: Too permissive
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return errors.New("invalid email")
|
||||
}
|
||||
// Also block injection patterns
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-601**: Open Redirect
|
||||
- **CWE-208**: Observable Timing Discrepancy
|
||||
- **CWE-611**: Improper Restriction of XML External Entity Reference
|
||||
- **CWE-770**: Allocation of Resources Without Limits
|
||||
- **CWE-20**: Improper Input Validation
|
||||
- **CWE-200**: Exposure of Sensitive Information
|
||||
@@ -0,0 +1,189 @@
|
||||
# Secrets Management Security Rules
|
||||
|
||||
Hardcoded secrets, credentials, and sensitive data in source code is a major security vulnerability.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. Secrets MUST be loaded from environment variables or secret managers.
|
||||
2. NEVER commit secrets to VCS.
|
||||
3. `.gitignore` MUST exclude secret files (`.env`, `*.key`, `*.pem`).
|
||||
|
||||
---
|
||||
|
||||
## Hardcoded Secrets and Credentials — Critical
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
const (
|
||||
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE" // DON'T
|
||||
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG" // DON'T
|
||||
DATABASE_PASSWORD = "SuperSecret123!" // DON'T
|
||||
JWT_SECRET = "my-super-secret-jwt-key" // DON'T
|
||||
)
|
||||
|
||||
var config = Config{
|
||||
APIKey: "abc123-xyz789-secret-key", // DON'T
|
||||
Secret: "my-super-secret-value", // DON'T
|
||||
DatabaseURL: "user:passw0rd!@localhost:5432/db", // DON'T
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
AWSAccessKey string
|
||||
AWSSecretKey string
|
||||
DatabasePassword string
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
cfg := &Config{
|
||||
AWSAccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
|
||||
AWSSecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
DatabasePassword: os.Getenv("DATABASE_PASSWORD"),
|
||||
JWTSecret: os.Getenv("JWT_SECRET"),
|
||||
}
|
||||
|
||||
if cfg.JWTSecret == "" {
|
||||
return nil, errors.New("JWT_SECRET is required")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hardcoded Database Passwords — Critical
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
// MySQL
|
||||
dsn := "user:Password123!@tcp(localhost:3306)/dbname" // DON'T
|
||||
|
||||
// PostgreSQL
|
||||
dsn := "user=postgres password=P@ssw0rd! dbname=mydb host=localhost" // DON'T
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
// MySQL
|
||||
func connectMySQL() (*sql.DB, error) {
|
||||
user := os.Getenv("DB_USER")
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
if password == "" {
|
||||
return nil, errors.New("DB_PASSWORD required")
|
||||
}
|
||||
host := getEnvWithDefault("DB_HOST", "localhost")
|
||||
port := getEnvWithDefault("DB_PORT", "3306")
|
||||
addr := net.JoinHostPort(host, port)
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s",
|
||||
user, password, addr, getEnvWithDefault("DB_NAME", "mydb"))
|
||||
return sql.Open("mysql", dsn)
|
||||
}
|
||||
|
||||
// PostgreSQL
|
||||
func connectPostgres() (*sql.DB, error) {
|
||||
connStr := os.Getenv("DATABASE_URL")
|
||||
if connStr == "" {
|
||||
return nil, errors.New("DATABASE_URL required")
|
||||
}
|
||||
return sql.Open("postgres", connStr)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secrets Storage Best Practices
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```go
|
||||
type EnvSecretLoader struct{}
|
||||
|
||||
func (l *EnvSecretLoader) Load(required []string) (map[string]string, error) {
|
||||
secrets := make(map[string]string)
|
||||
missing := []string{}
|
||||
for _, name := range required {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
missing = append(missing, name)
|
||||
continue
|
||||
}
|
||||
secrets[name] = value
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return nil, fmt.Errorf("missing: %v", missing)
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Secret Managers
|
||||
|
||||
```go
|
||||
type SecretManager interface {
|
||||
GetSecret(name string) (string, error)
|
||||
}
|
||||
|
||||
// AWS Secrets Manager
|
||||
type AWSSecretsManager struct {
|
||||
client *secretsmanager.Client
|
||||
}
|
||||
|
||||
func (m *AWSSecretsManager) GetSecret(name string) (string, error) {
|
||||
result, err := m.client.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(name),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.SecretString != nil {
|
||||
return *result.SecretString, nil
|
||||
}
|
||||
return string(result.SecretBinary), nil
|
||||
}
|
||||
```
|
||||
|
||||
### .gitignore Patterns
|
||||
|
||||
```
|
||||
# Secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
secrets/
|
||||
credentials/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secret Detection Patterns
|
||||
|
||||
| Pattern | Example |
|
||||
| --------------- | ------------------------------- |
|
||||
| API Keys | `Key = "sk_live_..."` |
|
||||
| Passwords | `password = "..."` |
|
||||
| Tokens | `token = "..."` |
|
||||
| Private Keys | `BEGIN PRIVATE KEY` |
|
||||
| AWS Credentials | `AWS_ACCESS_KEY_ID = "AKIA..."` |
|
||||
| JWT Secrets | `jwtSecret = "..."` |
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-798**: Use of Hard-coded Credentials
|
||||
- **CWE-312**: Cleartext Storage of Sensitive Information
|
||||
- **CWE-532**: Insertion of Sensitive Information into Log File
|
||||
- **CWE-359**: Exposure of Private Personal Information
|
||||
@@ -0,0 +1,159 @@
|
||||
# Third-Party Data Leak Rules
|
||||
|
||||
Third-party monitoring and analytics services can inadvertently transmit sensitive user data to external systems.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. PII MUST be filtered before sending to third-party services.
|
||||
2. Error tracking MUST NOT receive raw user data — use `BeforeSend` hooks to redact.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
These rules detect Go code that sends data to third-party services. Always review what data is being transmitted and ensure it complies with privacy regulations (GDPR, CCPA, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Common Vulnerable Services
|
||||
|
||||
| Service | Risk |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| Airbrake | Error tracking - sensitive data may be sent |
|
||||
| Bugsnag | Error tracking - sensitive data exposure |
|
||||
| Sentry | Error tracking - sensitive data in breadcrumbs/events |
|
||||
| Rollbar | Error tracking - sensitive data leaks |
|
||||
| Honeybadger | Error tracking - sensitive data leaks |
|
||||
| New Relic | Monitoring - sensitive data exposure |
|
||||
| Datadog | Monitoring - sensitive data in telemetry |
|
||||
| OpenTelemetry | Observability - sensitive data in traces/metrics |
|
||||
| Google Analytics | Analytics - PII tracking risks |
|
||||
| Algolia | Search API - data exfiltration risks |
|
||||
| Segment | Analytics - PII tracking risks |
|
||||
| BigQuery | Analytics - sensitive data in queries |
|
||||
| ClickHouse | Database - sensitive data queries |
|
||||
| Elasticsearch | Search engine - sensitive data in queries |
|
||||
|
||||
---
|
||||
|
||||
## Error Tracking Services — Medium
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
import "github.com/getsentry/sentry-go"
|
||||
|
||||
sentry.CaptureException(err) // DON'T: Captures full request context
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://xxx@sentry.io/123",
|
||||
RequestHeaders: []string{"Accept", "User-Agent"},
|
||||
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
|
||||
// Remove sensitive headers
|
||||
if event.Request != nil {
|
||||
delete(event.Request.Headers, "Authorization")
|
||||
delete(event.Request.Headers, "Cookie")
|
||||
}
|
||||
return event
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Analytics/Monitoring Services — Medium
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
analytics.Track("user_signed_up", analytics.Properties{
|
||||
"email": user.Email, // DON'T: PII!
|
||||
"phone": user.Phone, // DON'T: PII!
|
||||
"address": user.Address, // DON'T: PII!
|
||||
})
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
analytics.Track("user_signed_up", analytics.Properties{
|
||||
"user_id": user.ID, // OK: Internal identifier
|
||||
"plan": user.Plan, // OK: Business data
|
||||
"country": user.CountryCode, // OK: Non-identifying
|
||||
})
|
||||
|
||||
// Hash PII for correlation
|
||||
func hashEmail(email string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(email))
|
||||
return hex.EncodeToString(h.Sum(nil))[:8]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Filtering Layer
|
||||
|
||||
```go
|
||||
type DataFilter struct {
|
||||
sensitiveFields []string
|
||||
}
|
||||
|
||||
func NewDataFilter() *DataFilter {
|
||||
return &DataFilter{
|
||||
sensitiveFields: []string{
|
||||
"password", "token", "secret", "key", "email",
|
||||
"phone", "address", "ssn", "credit_card", "bank_account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DataFilter) Filter(data map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range data {
|
||||
keyLower := strings.ToLower(k)
|
||||
isSensitive := false
|
||||
for _, field := range f.sensitiveFields {
|
||||
if strings.Contains(keyLower, field) {
|
||||
isSensitive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isSensitive {
|
||||
result[k] = "[REDACTED]"
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Before integrating any third-party service:
|
||||
|
||||
- [ ] Identify what data is being sent
|
||||
- [ ] Remove any PII/PHI from transmitted data
|
||||
- [ ] Review data residency requirements
|
||||
- [ ] Implement data retention policies
|
||||
- [ ] Set up data export logging/auditing
|
||||
- [ ] Configure error handling to avoid data exposure
|
||||
- [ ] Review terms of service for data usage
|
||||
- [ ] Implement user consent management
|
||||
- [ ] Support data deletion requests
|
||||
- [ ] Conduct regular data flow audits
|
||||
|
||||
---
|
||||
|
||||
## CWE References
|
||||
|
||||
- **CWE-200**: Exposure of Sensitive Information
|
||||
- **CWE-359**: Exposure of Private Personal Information
|
||||
- **CWE-201**: Information Exposure Through Sent Data
|
||||
@@ -0,0 +1,189 @@
|
||||
# Threat Modeling Guide
|
||||
|
||||
Systematic methodology for identifying and prioritizing security threats in Go applications.
|
||||
|
||||
## STRIDE Methodology
|
||||
|
||||
Apply STRIDE to every element in your system's data flow diagram. Each element type is susceptible to specific threat categories:
|
||||
|
||||
### STRIDE per Element Matrix
|
||||
|
||||
| DFD Element | S | T | R | I | D | E |
|
||||
| ------------------------------------- | --- | --- | --- | --- | --- | --- |
|
||||
| External Entity (user, API client) | X | | X | | | |
|
||||
| Process (HTTP handler, gRPC service) | X | X | X | X | X | X |
|
||||
| Data Store (database, cache, file) | | X | X | X | X | |
|
||||
| Data Flow (HTTP, gRPC, message queue) | | X | | X | X | |
|
||||
|
||||
### Go-Specific STRIDE Analysis
|
||||
|
||||
**Spoofing** — Can an attacker impersonate a user or service?
|
||||
|
||||
```go
|
||||
// Check: Is every endpoint behind authentication?
|
||||
// Check: Are JWT tokens validated (algorithm, issuer, expiry)?
|
||||
// Check: Is mTLS configured for service-to-service calls?
|
||||
r.Use(authMiddleware) // every route group must have auth
|
||||
```
|
||||
|
||||
**Tampering** — Can data be modified in transit or at rest?
|
||||
|
||||
```go
|
||||
// Check: Are all external inputs validated?
|
||||
// Check: Is HMAC used for webhook/callback verification?
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(payload)
|
||||
expected := mac.Sum(nil)
|
||||
if !hmac.Equal(signature, expected) {
|
||||
return errors.New("tampered payload")
|
||||
}
|
||||
```
|
||||
|
||||
**Repudiation** — Can a user deny performing an action?
|
||||
|
||||
```go
|
||||
// Check: Are all security-relevant actions logged with structured data?
|
||||
logger.Info("action_performed",
|
||||
"user_id", userID,
|
||||
"action", "delete_account",
|
||||
"ip", r.RemoteAddr,
|
||||
"timestamp", time.Now().UTC(),
|
||||
)
|
||||
```
|
||||
|
||||
**Information Disclosure** — Can sensitive data leak?
|
||||
|
||||
```go
|
||||
// Check: Are error messages generic to clients?
|
||||
// Check: Are logs free of PII?
|
||||
// Check: Is TLS configured (no InsecureSkipVerify)?
|
||||
// Check: Are debug endpoints (pprof) disabled in production?
|
||||
```
|
||||
|
||||
**Denial of Service** — Can the service be overwhelmed?
|
||||
|
||||
```go
|
||||
// Check: Are timeouts set on the HTTP server?
|
||||
// Check: Are request body sizes limited?
|
||||
// Check: Is rate limiting in place?
|
||||
server := &http.Server{
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB
|
||||
}
|
||||
```
|
||||
|
||||
**Elevation of Privilege** — Can a user gain unauthorized access?
|
||||
|
||||
```go
|
||||
// Check: Is authorization checked server-side on every request?
|
||||
// Check: Are object references validated (no IDOR)?
|
||||
// Check: Are admin routes properly protected?
|
||||
if !user.HasPermission("admin:write") {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DREAD Risk Scoring
|
||||
|
||||
Score each identified threat to prioritize remediation:
|
||||
|
||||
| Factor | 1-3 (Low) | 4-6 (Medium) | 7-10 (High) |
|
||||
| --- | --- | --- | --- |
|
||||
| **D**amage | Minor info disclosure | Partial data breach | Full system compromise, data destruction |
|
||||
| **R**eproducibility | Timing-dependent, hard to reproduce | Reproducible with some effort | Always reproducible, automated tools exist |
|
||||
| **E**xploitability | Custom exploit, advanced skills needed | Basic tools available | No skills required, public exploit exists |
|
||||
| **A**ffected users | Individual user | Subset of users | All users |
|
||||
| **D**iscoverability | Requires insider knowledge | Found via scanning | Publicly documented, obvious |
|
||||
|
||||
**Score** = (D + R + E + A + D) / 5. Risk levels: **8-10 Critical**, **6-7.9 High**, **4-5.9 Medium**, **1-3.9 Low**.
|
||||
|
||||
### Example: SQL Injection in Login Handler
|
||||
|
||||
| Factor | Score | Justification |
|
||||
| --------------- | ----- | ------------------------------------------ |
|
||||
| Damage | 9 | Full database access, credential theft |
|
||||
| Reproducibility | 9 | Consistent, automated tools exist (sqlmap) |
|
||||
| Exploitability | 8 | Well-documented attack, easy tooling |
|
||||
| Affected Users | 10 | All users with accounts |
|
||||
| Discoverability | 7 | Automated scanners detect easily |
|
||||
|
||||
**DREAD Score: 8.6 — Critical. Immediate remediation required.**
|
||||
|
||||
---
|
||||
|
||||
## Trust Boundary Analysis
|
||||
|
||||
Map where untrusted data enters your Go application:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ TRUST BOUNDARY │
|
||||
│ │
|
||||
Internet ──→ [LB/WAF] ──→ [Go HTTP Server] │
|
||||
│ │ │
|
||||
│ [Middleware] │
|
||||
│ - Auth (JWT/session) │
|
||||
│ - Rate limiting │
|
||||
│ - Input validation │
|
||||
│ - Security headers │
|
||||
│ │ │
|
||||
│ [Service Layer] ──→ [Cache] │
|
||||
│ │ │
|
||||
│ [Database] (parameterized queries) │
|
||||
│ │
|
||||
└──────────┬──────────────────────────┘
|
||||
│
|
||||
External APIs (mTLS)
|
||||
```
|
||||
|
||||
Every arrow crossing the trust boundary needs:
|
||||
|
||||
1. **Authentication** — who is making this request?
|
||||
2. **Input validation** — is the data well-formed and within bounds?
|
||||
3. **Authorization** — is this caller allowed to perform this action on this resource?
|
||||
|
||||
---
|
||||
|
||||
## OWASP Top 10 Mapping for Go
|
||||
|
||||
| Rank | Vulnerability | STRIDE | Go Defense |
|
||||
| --- | --- | --- | --- |
|
||||
| A01 | Broken Access Control | E | Server-side authz middleware, RBAC, IDOR checks |
|
||||
| A02 | Cryptographic Failures | I | `crypto/aes` GCM, `crypto/rand`, TLS 1.2+ |
|
||||
| A03 | Injection | T, E | `database/sql` placeholders, `exec.Command` separate args, `html/template` |
|
||||
| A04 | Insecure Design | All | Threat modeling with STRIDE, defense-in-depth |
|
||||
| A05 | Security Misconfiguration | I, E | Server timeouts, TLS config, no `InsecureSkipVerify`, no exposed pprof |
|
||||
| A06 | Vulnerable Components | All | `govulncheck`, Dependabot/Renovate, `go.sum` verification |
|
||||
| A07 | Authentication Failures | S, E | Argon2id/bcrypt, JWT validation (algorithm pinning), MFA |
|
||||
| A08 | Software/Data Integrity | T | Module checksums (`go.sum`), signed releases, CI verification |
|
||||
| A09 | Logging Failures | R | Structured logging (`log/slog`), audit trails, no PII |
|
||||
| A10 | SSRF | I, T | URL allowlists, block internal IPs and metadata endpoints |
|
||||
|
||||
---
|
||||
|
||||
## Conducting a Threat Model
|
||||
|
||||
1. **Scope** — identify system boundaries, assets to protect, and threat actors
|
||||
2. **Diagram** — draw a data flow diagram with trust boundaries (external entities, processes, data stores, data flows)
|
||||
3. **STRIDE** — apply STRIDE to each DFD element using the matrix above
|
||||
4. **Score** — rate each threat with DREAD
|
||||
5. **Prioritize** — fix Critical/High first; document accepted risks with explicit justification
|
||||
6. **Verify** — run `gosec ./...`, `govulncheck ./...`, `go test -race ./...` to validate mitigations
|
||||
7. **Iterate** — update the model when the system changes (new endpoints, new data flows, new integrations)
|
||||
|
||||
---
|
||||
|
||||
## Vulnerability Severity Matrix
|
||||
|
||||
Use when no DREAD data is available — cross-reference impact with exploitability:
|
||||
|
||||
| Impact \ Exploitability | Easy | Moderate | Difficult |
|
||||
| ----------------------- | -------- | -------- | --------- |
|
||||
| Critical | Critical | Critical | High |
|
||||
| High | Critical | High | Medium |
|
||||
| Medium | High | Medium | Low |
|
||||
| Low | Medium | Low | Low |
|
||||
Reference in New Issue
Block a user