1595 lines
47 KiB
Markdown
1595 lines
47 KiB
Markdown
|
|
# Security Audit Report - CV Application
|
|||
|
|
**Date:** 2025-11-30
|
|||
|
|
**Auditor:** Security Architecture Expert
|
|||
|
|
**Application:** Go/HTMX CV Portfolio Site
|
|||
|
|
**Scope:** Full application security review + Contact form security design
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Executive Summary
|
|||
|
|
|
|||
|
|
### Overall Security Posture: **GOOD** ✅
|
|||
|
|
The application demonstrates solid security practices with proper use of Go's `html/template` for XSS prevention, comprehensive security headers, rate limiting, and origin validation. No critical vulnerabilities were identified in the current codebase.
|
|||
|
|
|
|||
|
|
### Key Findings:
|
|||
|
|
- ✅ **No SQL Injection Risk** - No database usage
|
|||
|
|
- ✅ **XSS Protection** - Proper use of `html/template` with auto-escaping
|
|||
|
|
- ✅ **Command Injection Prevention** - Uses `go-git` library instead of shell commands
|
|||
|
|
- ✅ **Security Headers** - Comprehensive CSP, HSTS, X-Frame-Options configured
|
|||
|
|
- ✅ **Rate Limiting** - PDF endpoint properly rate-limited (3/min)
|
|||
|
|
- ✅ **Origin Validation** - Implemented for PDF endpoint
|
|||
|
|
- ⚠️ **CSRF Protection** - Missing for POST endpoints (HTMX toggles)
|
|||
|
|
- ⚠️ **Security Logging** - Basic logging, needs security event tracking
|
|||
|
|
- ⚠️ **Contact Form** - Not yet implemented (design provided below)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Files Reviewed
|
|||
|
|
|
|||
|
|
### Core Application Files
|
|||
|
|
1. `/internal/middleware/security.go` - Security headers, rate limiting, origin validation
|
|||
|
|
2. `/internal/middleware/logger.go` - Request logging
|
|||
|
|
3. `/internal/middleware/recovery.go` - Panic recovery
|
|||
|
|
4. `/internal/handlers/*.go` - All HTTP handlers
|
|||
|
|
5. `/internal/templates/template.go` - Template rendering
|
|||
|
|
6. `/internal/pdf/generator.go` - PDF generation
|
|||
|
|
7. `/internal/routes/routes.go` - Routing configuration
|
|||
|
|
8. `/main.go` - Server initialization
|
|||
|
|
9. `/templates/*.html` - All HTML templates
|
|||
|
|
|
|||
|
|
### Security Test Files
|
|||
|
|
1. `/internal/handlers/cv_security_test.go` - Path traversal prevention tests
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Vulnerability Assessment (OWASP Top 10 2021)
|
|||
|
|
|
|||
|
|
### A01: Broken Access Control ✅ SECURE
|
|||
|
|
**Status:** No vulnerabilities found
|
|||
|
|
|
|||
|
|
**Current Controls:**
|
|||
|
|
- Origin validation on PDF endpoint prevents unauthorized external access
|
|||
|
|
- Rate limiting (3 requests/minute) prevents abuse
|
|||
|
|
- Path validation prevents directory traversal attacks
|
|||
|
|
- No authentication/authorization required (public portfolio site)
|
|||
|
|
|
|||
|
|
**Evidence:**
|
|||
|
|
```go
|
|||
|
|
// Origin validation implemented
|
|||
|
|
func OriginChecker(next http.Handler) http.Handler {
|
|||
|
|
// Validates Origin and Referer headers
|
|||
|
|
// Blocks external access to resource-intensive endpoints
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Path traversal prevention
|
|||
|
|
func validateRepoPath(path string) error {
|
|||
|
|
// Ensures path is within project directory
|
|||
|
|
// Prevents ../../../etc/passwd attacks
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Test Results:**
|
|||
|
|
```bash
|
|||
|
|
✅ PASS: TestValidateRepoPath (path traversal prevention)
|
|||
|
|
✅ PASS: TestGetGitRepoFirstCommitDate_SecurityValidation
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A02: Cryptographic Failures ✅ SECURE
|
|||
|
|
**Status:** No sensitive data storage, proper TLS configuration
|
|||
|
|
|
|||
|
|
**Current Controls:**
|
|||
|
|
- HSTS header enforced in production (1 year, includeSubDomains, preload)
|
|||
|
|
- No passwords, API keys, or secrets in codebase
|
|||
|
|
- Environment variables used for configuration
|
|||
|
|
- TLS termination recommended at reverse proxy (Nginx)
|
|||
|
|
|
|||
|
|
**Recommendations:**
|
|||
|
|
- ✅ Already using `.env` file (not committed to git)
|
|||
|
|
- ✅ `.env.example` provided without secrets
|
|||
|
|
- ⚠️ Ensure production uses strong TLS cipher suites (see Nginx config below)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A03: Injection ✅ SECURE
|
|||
|
|
**Status:** No injection vulnerabilities found
|
|||
|
|
|
|||
|
|
#### SQL Injection: N/A
|
|||
|
|
- **No database usage** - Application reads from JSON files only
|
|||
|
|
- Static data in `/data/cv-{lang}.json`
|
|||
|
|
|
|||
|
|
#### XSS (Cross-Site Scripting): SECURE ✅
|
|||
|
|
**Template Auto-Escaping:**
|
|||
|
|
```go
|
|||
|
|
// Go's html/template automatically escapes all variables
|
|||
|
|
{{.CV.Personal.Name}} // Auto-escaped
|
|||
|
|
{{.CV.Personal.Email}} // Auto-escaped
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SafeHTML Usage - CONTROLLED:**
|
|||
|
|
```go
|
|||
|
|
// Only used for trusted CV YAML content, never user input
|
|||
|
|
"safeHTML": func(s string) template.HTML {
|
|||
|
|
return template.HTML(s)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Verification:**
|
|||
|
|
- All user-facing data passes through `html/template`
|
|||
|
|
- No `innerHTML`, `eval()`, or dangerous DOM manipulation
|
|||
|
|
- CSP header blocks inline scripts (except trusted sources)
|
|||
|
|
|
|||
|
|
#### Command Injection: SECURE ✅
|
|||
|
|
**go-git Library Usage:**
|
|||
|
|
```go
|
|||
|
|
// Uses pure Go library, NO shell commands
|
|||
|
|
repo, err := git.PlainOpen(repoPath)
|
|||
|
|
// Instead of: exec.Command("git", "log", repoPath)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Security Tests:**
|
|||
|
|
```bash
|
|||
|
|
✅ Malicious paths rejected: "../../../etc/passwd", "data | cat /etc/passwd"
|
|||
|
|
✅ Command injection attempts blocked: "data; rm -rf /", "data`whoami`"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A04: Insecure Design ⚠️ NEEDS IMPROVEMENT
|
|||
|
|
**Status:** Generally secure, CSRF protection needed
|
|||
|
|
|
|||
|
|
**Current State:**
|
|||
|
|
- ✅ Rate limiting on resource-intensive endpoints
|
|||
|
|
- ✅ Origin validation prevents hotlinking
|
|||
|
|
- ⚠️ **MISSING:** CSRF tokens for POST endpoints
|
|||
|
|
- ⚠️ **MISSING:** Security event logging
|
|||
|
|
|
|||
|
|
**CSRF Vulnerability - POST Endpoints:**
|
|||
|
|
```go
|
|||
|
|
// VULNERABLE: No CSRF protection
|
|||
|
|
POST /toggle/length
|
|||
|
|
POST /toggle/icons
|
|||
|
|
POST /toggle/theme
|
|||
|
|
POST /switch-language
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Impact:** Low (only changes user preferences, no data modification)
|
|||
|
|
|
|||
|
|
**Recommended Fix:**
|
|||
|
|
1. Implement CSRF token generation and validation
|
|||
|
|
2. Add token to all POST requests via HTMX
|
|||
|
|
3. Validate token in middleware
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A05: Security Misconfiguration ✅ MOSTLY SECURE
|
|||
|
|
**Status:** Good security headers, minor improvements needed
|
|||
|
|
|
|||
|
|
**Current Security Headers (Excellent):**
|
|||
|
|
```go
|
|||
|
|
// Strong CSP policy
|
|||
|
|
Content-Security-Policy: default-src 'self';
|
|||
|
|
script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net;
|
|||
|
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
// Clickjacking protection
|
|||
|
|
X-Frame-Options: SAMEORIGIN
|
|||
|
|
|
|||
|
|
// MIME sniffing prevention
|
|||
|
|
X-Content-Type-Options: nosniff
|
|||
|
|
|
|||
|
|
// HSTS (production only)
|
|||
|
|
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
|||
|
|
|
|||
|
|
// Privacy protection
|
|||
|
|
Referrer-Policy: strict-origin-when-cross-origin
|
|||
|
|
|
|||
|
|
// Feature policy
|
|||
|
|
Permissions-Policy: geolocation=(), microphone=(), camera=(), ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Recommendations:**
|
|||
|
|
1. ✅ CSP is comprehensive
|
|||
|
|
2. ⚠️ Consider tightening `'unsafe-inline'` for scripts (use nonces)
|
|||
|
|
3. ✅ HSTS properly configured for production
|
|||
|
|
4. ⚠️ Add `X-Permitted-Cross-Domain-Policies: none`
|
|||
|
|
5. ⚠️ Add `Cross-Origin-Opener-Policy: same-origin`
|
|||
|
|
6. ⚠️ Add `Cross-Origin-Embedder-Policy: require-corp`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A06: Vulnerable and Outdated Components ⚠️ CHECK REGULARLY
|
|||
|
|
**Status:** Dependencies need regular auditing
|
|||
|
|
|
|||
|
|
**Current Dependencies:**
|
|||
|
|
```go
|
|||
|
|
// go.mod
|
|||
|
|
chromedp/chromedp v0.14.2 // PDF generation
|
|||
|
|
joho/godotenv v1.5.1 // Environment variables
|
|||
|
|
go-git/go-git v5.16.4 // Git operations (no shell commands)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Recommendations:**
|
|||
|
|
1. Run `go list -m -u all` regularly for updates
|
|||
|
|
2. Subscribe to security advisories for:
|
|||
|
|
- chromedp (Chromium vulnerabilities)
|
|||
|
|
- go-git (Git parsing vulnerabilities)
|
|||
|
|
3. Implement automated dependency scanning (Dependabot/Snyk)
|
|||
|
|
|
|||
|
|
**Frontend Dependencies:**
|
|||
|
|
```javascript
|
|||
|
|
// index.html - Using CDN with SRI
|
|||
|
|
htmx.org@1.9.10 (SRI: sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX...)
|
|||
|
|
hyperscript.org@0.9.14 (no SRI - ADD THIS)
|
|||
|
|
iconify-icon@2.1.0 (no SRI - ADD THIS)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Action Items:**
|
|||
|
|
- ⚠️ Add SRI hashes for all CDN scripts
|
|||
|
|
- ⚠️ Update hyperscript to latest version
|
|||
|
|
- ⚠️ Monitor HTMX security advisories
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A07: Identification and Authentication Failures N/A
|
|||
|
|
**Status:** No authentication system (public portfolio)
|
|||
|
|
|
|||
|
|
**Rationale:**
|
|||
|
|
- Public CV portfolio site - no login required
|
|||
|
|
- No user accounts or sessions
|
|||
|
|
- Cookies only store UI preferences (non-sensitive)
|
|||
|
|
|
|||
|
|
**Future Contact Form:**
|
|||
|
|
- Will require email validation
|
|||
|
|
- Rate limiting per IP (5/hour recommended)
|
|||
|
|
- Honeypot + timing validation for bot prevention
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A08: Software and Data Integrity Failures ⚠️ NEEDS IMPROVEMENT
|
|||
|
|
**Status:** Missing SRI for some CDN resources
|
|||
|
|
|
|||
|
|
**Current State:**
|
|||
|
|
- ✅ HTMX loaded with SRI hash
|
|||
|
|
- ⚠️ Hyperscript missing SRI
|
|||
|
|
- ⚠️ Iconify missing SRI
|
|||
|
|
- ✅ No code integrity checks (not needed for static Go binary)
|
|||
|
|
|
|||
|
|
**Recommendations:**
|
|||
|
|
```html
|
|||
|
|
<!-- Add SRI hashes -->
|
|||
|
|
<script src="https://unpkg.com/hyperscript.org@0.9.14"
|
|||
|
|
integrity="sha384-[GENERATE_SRI_HASH]"
|
|||
|
|
crossorigin="anonymous"></script>
|
|||
|
|
|
|||
|
|
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"
|
|||
|
|
integrity="sha384-[GENERATE_SRI_HASH]"
|
|||
|
|
crossorigin="anonymous"></script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Generate SRI:** https://www.srihash.org/
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A09: Security Logging and Monitoring ⚠️ NEEDS IMPROVEMENT
|
|||
|
|
**Status:** Basic logging, needs security event tracking
|
|||
|
|
|
|||
|
|
**Current Logging:**
|
|||
|
|
```go
|
|||
|
|
// Basic request logging
|
|||
|
|
log.Printf("[%s] %s %s - %d (%v)", r.Method, r.URL.Path, r.RemoteAddr, status, duration)
|
|||
|
|
|
|||
|
|
// Error logging
|
|||
|
|
log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, err)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Missing Security Events:**
|
|||
|
|
- ❌ Rate limit violations
|
|||
|
|
- ❌ Origin validation failures
|
|||
|
|
- ❌ CSRF validation failures (when implemented)
|
|||
|
|
- ❌ Suspicious request patterns
|
|||
|
|
- ❌ PDF generation failures (could indicate attack)
|
|||
|
|
|
|||
|
|
**Recommendations:** See Security Logging section below
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### A10: Server-Side Request Forgery (SSRF) ✅ SECURE
|
|||
|
|
**Status:** No SSRF vulnerability
|
|||
|
|
|
|||
|
|
**Analysis:**
|
|||
|
|
- PDF generation uses internal server address only
|
|||
|
|
- No user-controlled URLs in `chromedp.Navigate()`
|
|||
|
|
- No external HTTP requests from user input
|
|||
|
|
|
|||
|
|
**Current Implementation:**
|
|||
|
|
```go
|
|||
|
|
// Hardcoded server address, not user-controlled
|
|||
|
|
targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, req.Lang)
|
|||
|
|
pdfData, err := h.pdfGenerator.GenerateFromURLWithOptions(ctx, targetURL, cookies, renderMode)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**No SSRF risk - URL is constructed server-side with validated parameters only.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Strengths 💪
|
|||
|
|
|
|||
|
|
### 1. Template Security (XSS Prevention)
|
|||
|
|
- ✅ Go's `html/template` auto-escapes all variables
|
|||
|
|
- ✅ `safeHTML` only used for trusted CV content
|
|||
|
|
- ✅ No user input rendered without escaping
|
|||
|
|
- ✅ CSP blocks inline scripts (except whitelisted CDNs)
|
|||
|
|
|
|||
|
|
### 2. Command Injection Prevention
|
|||
|
|
- ✅ Uses `go-git` library instead of `exec.Command`
|
|||
|
|
- ✅ Path traversal prevention with `validateRepoPath()`
|
|||
|
|
- ✅ Comprehensive security tests for malicious paths
|
|||
|
|
|
|||
|
|
### 3. Security Headers (Best-in-Class)
|
|||
|
|
- ✅ Content Security Policy (CSP)
|
|||
|
|
- ✅ HTTP Strict Transport Security (HSTS)
|
|||
|
|
- ✅ X-Frame-Options (clickjacking prevention)
|
|||
|
|
- ✅ X-Content-Type-Options (MIME sniffing prevention)
|
|||
|
|
- ✅ Referrer-Policy (privacy)
|
|||
|
|
- ✅ Permissions-Policy (feature restrictions)
|
|||
|
|
|
|||
|
|
### 4. Rate Limiting
|
|||
|
|
- ✅ Implemented on PDF endpoint (3 requests/minute)
|
|||
|
|
- ✅ In-memory rate limiter with automatic cleanup
|
|||
|
|
- ✅ Proper 429 Too Many Requests response
|
|||
|
|
|
|||
|
|
### 5. Origin Validation
|
|||
|
|
- ✅ Prevents external hotlinking of PDF endpoint
|
|||
|
|
- ✅ Validates Origin and Referer headers
|
|||
|
|
- ✅ Configurable via ALLOWED_ORIGINS env variable
|
|||
|
|
|
|||
|
|
### 6. Input Validation
|
|||
|
|
- ✅ Strict validation for PDF export parameters
|
|||
|
|
- ✅ Language: only "en" or "es" allowed
|
|||
|
|
- ✅ Length, icons, version parameters validated
|
|||
|
|
- ✅ Rejects invalid inputs with proper error messages
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Weaknesses & Recommendations
|
|||
|
|
|
|||
|
|
### 1. CSRF Protection - MISSING ⚠️
|
|||
|
|
**Priority:** Medium
|
|||
|
|
**Severity:** Low (no sensitive data, only UI preferences)
|
|||
|
|
|
|||
|
|
**Vulnerable Endpoints:**
|
|||
|
|
```
|
|||
|
|
POST /toggle/length
|
|||
|
|
POST /toggle/icons
|
|||
|
|
POST /toggle/theme
|
|||
|
|
POST /switch-language
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Recommendation:** Implement CSRF token system (see implementation below)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Security Logging - INSUFFICIENT ⚠️
|
|||
|
|
**Priority:** High
|
|||
|
|
**Severity:** Medium
|
|||
|
|
|
|||
|
|
**Missing Events:**
|
|||
|
|
- Rate limit violations (potential attack indicators)
|
|||
|
|
- Origin validation failures (hotlinking attempts)
|
|||
|
|
- Repeated failed requests (reconnaissance/scanning)
|
|||
|
|
- Suspicious user agents (bots, scrapers)
|
|||
|
|
|
|||
|
|
**Recommendation:** Implement structured security logging (see implementation below)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. Subresource Integrity - INCOMPLETE ⚠️
|
|||
|
|
**Priority:** Medium
|
|||
|
|
**Severity:** Low
|
|||
|
|
|
|||
|
|
**Missing SRI Hashes:**
|
|||
|
|
- Hyperscript library
|
|||
|
|
- Iconify library
|
|||
|
|
|
|||
|
|
**Recommendation:** Add SRI hashes for all CDN resources
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Input Sanitization for Future Contact Form ⚠️
|
|||
|
|
**Priority:** CRITICAL (when implementing contact form)
|
|||
|
|
|
|||
|
|
**Required Security Controls:**
|
|||
|
|
1. Email header injection prevention
|
|||
|
|
2. Strict input validation (see design below)
|
|||
|
|
3. Rate limiting (5 requests/hour per IP)
|
|||
|
|
4. Bot protection (honeypot + timing)
|
|||
|
|
5. **Origin validation (BROWSER-ONLY access)**
|
|||
|
|
6. CSRF token validation
|
|||
|
|
7. Email address validation (RFC 5322)
|
|||
|
|
8. Content-Type enforcement
|
|||
|
|
|
|||
|
|
**See Contact Form Security Design below for complete implementation.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Contact Form Security Design 🔐
|
|||
|
|
|
|||
|
|
### CRITICAL REQUIREMENT: Browser-Only Access
|
|||
|
|
The contact form endpoint MUST reject all non-browser requests:
|
|||
|
|
- ❌ Block: curl, Postman, wget, Python requests, HTTPie, etc.
|
|||
|
|
- ✅ Allow: Only genuine browser requests from the same origin
|
|||
|
|
|
|||
|
|
### Security Architecture
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Browser → [Origin Validation] → [CSRF Token] → [Rate Limit] →
|
|||
|
|
→ [Bot Detection] → [Input Validation] → [Email Sanitization] → [Send Email]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1. Origin Validation (CRITICAL)
|
|||
|
|
```go
|
|||
|
|
// BrowserOnlyMiddleware - Blocks non-browser requests
|
|||
|
|
func BrowserOnlyMiddleware(next http.Handler) http.Handler {
|
|||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
// Check 1: Require Origin or Referer header
|
|||
|
|
origin := r.Header.Get("Origin")
|
|||
|
|
referer := r.Header.Get("Referer")
|
|||
|
|
|
|||
|
|
if origin == "" && referer == "" {
|
|||
|
|
// No Origin/Referer = command-line tool (curl, wget)
|
|||
|
|
logSecurityEvent("BLOCKED", r, "Missing Origin/Referer headers")
|
|||
|
|
http.Error(w, "Forbidden: Browser access required", http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check 2: Validate origin matches allowed domains
|
|||
|
|
if !isAllowedOrigin(origin, allowedOrigins) && !isAllowedOrigin(referer, allowedOrigins) {
|
|||
|
|
logSecurityEvent("BLOCKED", r, "Invalid origin: " + origin)
|
|||
|
|
http.Error(w, "Forbidden: Invalid origin", http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check 3: Require X-Requested-With header (AJAX/HTMX)
|
|||
|
|
requestedWith := r.Header.Get("X-Requested-With")
|
|||
|
|
if requestedWith != "XMLHttpRequest" && r.Header.Get("HX-Request") == "" {
|
|||
|
|
logSecurityEvent("BLOCKED", r, "Missing X-Requested-With/HX-Request")
|
|||
|
|
http.Error(w, "Forbidden: AJAX/HTMX request required", http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check 4: User-Agent validation (reject obvious bots)
|
|||
|
|
ua := r.Header.Get("User-Agent")
|
|||
|
|
if isSuspiciousUserAgent(ua) {
|
|||
|
|
logSecurityEvent("BLOCKED", r, "Suspicious User-Agent: " + ua)
|
|||
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next.ServeHTTP(w, r)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func isSuspiciousUserAgent(ua string) bool {
|
|||
|
|
ua = strings.ToLower(ua)
|
|||
|
|
|
|||
|
|
// Block common command-line tools
|
|||
|
|
blockedAgents := []string{
|
|||
|
|
"curl", "wget", "python-requests", "postman",
|
|||
|
|
"httpie", "insomnia", "paw", "rest-client",
|
|||
|
|
"apache-httpclient", "java/", "go-http-client",
|
|||
|
|
"libwww-perl", "axios", "node-fetch",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, blocked := range blockedAgents {
|
|||
|
|
if strings.Contains(ua, blocked) {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Empty User-Agent (common for scripts)
|
|||
|
|
if ua == "" {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Input Validation
|
|||
|
|
```go
|
|||
|
|
type ContactFormRequest struct {
|
|||
|
|
Name string `json:"name" validate:"required,max=100,alpha_space"`
|
|||
|
|
Email string `json:"email" validate:"required,email,max=254"`
|
|||
|
|
Company string `json:"company" validate:"max=100,alphanum_space"`
|
|||
|
|
Subject string `json:"subject" validate:"required,max=200,safe_chars"`
|
|||
|
|
Message string `json:"message" validate:"required,max=5000"`
|
|||
|
|
|
|||
|
|
// Bot detection fields
|
|||
|
|
Honeypot string `json:"website"` // Should be empty
|
|||
|
|
Timestamp int64 `json:"timestamp"` // Form load time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ValidateContactForm performs comprehensive validation
|
|||
|
|
func ValidateContactForm(req *ContactFormRequest) error {
|
|||
|
|
// Honeypot check
|
|||
|
|
if req.Honeypot != "" {
|
|||
|
|
return errors.New("bot detected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Timing check (must take at least 2 seconds)
|
|||
|
|
now := time.Now().Unix()
|
|||
|
|
if now - req.Timestamp < 2 {
|
|||
|
|
return errors.New("form submitted too quickly")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Email validation (RFC 5322)
|
|||
|
|
if !isValidEmail(req.Email) {
|
|||
|
|
return errors.New("invalid email address")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Email header injection prevention
|
|||
|
|
if containsEmailInjection(req.Name) || containsEmailInjection(req.Email) ||
|
|||
|
|
containsEmailInjection(req.Subject) {
|
|||
|
|
return errors.New("invalid characters in email fields")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Name validation (alphanumeric + spaces only)
|
|||
|
|
if !regexp.MustCompile(`^[a-zA-Z\s'-]+$`).MatchString(req.Name) {
|
|||
|
|
return errors.New("invalid name format")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Subject validation (safe characters only)
|
|||
|
|
if !regexp.MustCompile(`^[a-zA-Z0-9\s.,!?-]+$`).MatchString(req.Subject) {
|
|||
|
|
return errors.New("invalid subject format")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Message sanitization (strip HTML tags)
|
|||
|
|
req.Message = stripHTMLTags(req.Message)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// containsEmailInjection checks for email header injection attempts
|
|||
|
|
func containsEmailInjection(s string) bool {
|
|||
|
|
// Check for newlines (header injection)
|
|||
|
|
if strings.ContainsAny(s, "\r\n") {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check for email header patterns
|
|||
|
|
dangerousPatterns := []string{
|
|||
|
|
"Content-Type:", "MIME-Version:", "Content-Transfer-Encoding:",
|
|||
|
|
"bcc:", "cc:", "to:", "from:",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sLower := strings.ToLower(s)
|
|||
|
|
for _, pattern := range dangerousPatterns {
|
|||
|
|
if strings.Contains(sLower, pattern) {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func isValidEmail(email string) bool {
|
|||
|
|
// RFC 5322 regex (simplified)
|
|||
|
|
pattern := `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`
|
|||
|
|
matched, _ := regexp.MatchString(pattern, email)
|
|||
|
|
return matched && len(email) <= 254
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func stripHTMLTags(s string) string {
|
|||
|
|
// Remove all HTML tags
|
|||
|
|
re := regexp.MustCompile(`<[^>]*>`)
|
|||
|
|
return re.ReplaceAllString(s, "")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Rate Limiting (Strict)
|
|||
|
|
```go
|
|||
|
|
// Contact form rate limiter: 5 requests per hour per IP
|
|||
|
|
contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)
|
|||
|
|
|
|||
|
|
// Apply to contact endpoint
|
|||
|
|
protectedContactHandler := middleware.BrowserOnlyMiddleware(
|
|||
|
|
middleware.CSRFProtection(
|
|||
|
|
contactRateLimiter.Middleware(
|
|||
|
|
http.HandlerFunc(contactHandler.SendMessage),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
mux.Handle("/api/contact", protectedContactHandler)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. CSRF Protection
|
|||
|
|
```go
|
|||
|
|
// Generate CSRF token on page load
|
|||
|
|
func (h *ContactHandler) ShowContactForm(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
token := generateCSRFToken()
|
|||
|
|
|
|||
|
|
// Set token in secure cookie
|
|||
|
|
http.SetCookie(w, &http.Cookie{
|
|||
|
|
Name: "csrf_token",
|
|||
|
|
Value: token,
|
|||
|
|
Path: "/",
|
|||
|
|
HttpOnly: true,
|
|||
|
|
Secure: true, // HTTPS only
|
|||
|
|
SameSite: http.SameSiteStrictMode,
|
|||
|
|
MaxAge: 3600, // 1 hour
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Also pass token to template for hidden field
|
|||
|
|
data := map[string]interface{}{
|
|||
|
|
"CSRFToken": token,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.templates.Render(w, "contact.html", data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Validate CSRF token on submission
|
|||
|
|
func validateCSRFToken(r *http.Request) error {
|
|||
|
|
cookie, err := r.Cookie("csrf_token")
|
|||
|
|
if err != nil {
|
|||
|
|
return errors.New("missing CSRF token cookie")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
formToken := r.FormValue("csrf_token")
|
|||
|
|
if formToken == "" {
|
|||
|
|
return errors.New("missing CSRF token in request")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !secureCompare(cookie.Value, formToken) {
|
|||
|
|
return errors.New("CSRF token mismatch")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Constant-time comparison to prevent timing attacks
|
|||
|
|
func secureCompare(a, b string) bool {
|
|||
|
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func generateCSRFToken() string {
|
|||
|
|
b := make([]byte, 32)
|
|||
|
|
rand.Read(b)
|
|||
|
|
return base64.URLEncoding.EncodeToString(b)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. Email Sanitization
|
|||
|
|
```go
|
|||
|
|
func sanitizeEmailContent(req *ContactFormRequest) {
|
|||
|
|
// Remove all newlines from header fields
|
|||
|
|
req.Name = strings.ReplaceAll(req.Name, "\r", "")
|
|||
|
|
req.Name = strings.ReplaceAll(req.Name, "\n", "")
|
|||
|
|
req.Email = strings.ReplaceAll(req.Email, "\r", "")
|
|||
|
|
req.Email = strings.ReplaceAll(req.Email, "\n", "")
|
|||
|
|
req.Subject = strings.ReplaceAll(req.Subject, "\r", "")
|
|||
|
|
req.Subject = strings.ReplaceAll(req.Subject, "\n", "")
|
|||
|
|
|
|||
|
|
// Trim whitespace
|
|||
|
|
req.Name = strings.TrimSpace(req.Name)
|
|||
|
|
req.Email = strings.TrimSpace(req.Email)
|
|||
|
|
req.Company = strings.TrimSpace(req.Company)
|
|||
|
|
req.Subject = strings.TrimSpace(req.Subject)
|
|||
|
|
req.Message = strings.TrimSpace(req.Message)
|
|||
|
|
|
|||
|
|
// HTML entity encoding for message body
|
|||
|
|
req.Message = template.HTMLEscapeString(req.Message)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6. Complete Contact Handler
|
|||
|
|
```go
|
|||
|
|
func (h *ContactHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
// Parse request
|
|||
|
|
var req ContactFormRequest
|
|||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|||
|
|
HandleError(w, r, BadRequestError("Invalid request format"))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Set timestamp from server (don't trust client)
|
|||
|
|
req.Timestamp = time.Now().Unix()
|
|||
|
|
|
|||
|
|
// Validate CSRF token
|
|||
|
|
if err := validateCSRFToken(r); err != nil {
|
|||
|
|
logSecurityEvent("CSRF_VIOLATION", r, err.Error())
|
|||
|
|
HandleError(w, r, ForbiddenError("CSRF validation failed"))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Validate input
|
|||
|
|
if err := ValidateContactForm(&req); err != nil {
|
|||
|
|
logSecurityEvent("VALIDATION_FAILED", r, err.Error())
|
|||
|
|
HandleError(w, r, BadRequestError(err.Error()))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sanitize content
|
|||
|
|
sanitizeEmailContent(&req)
|
|||
|
|
|
|||
|
|
// Send email (using standard library or third-party service)
|
|||
|
|
if err := h.emailService.Send(req); err != nil {
|
|||
|
|
logSecurityEvent("EMAIL_SEND_FAILED", r, err.Error())
|
|||
|
|
HandleError(w, r, InternalError(err))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Log success
|
|||
|
|
logSecurityEvent("CONTACT_FORM_SENT", r, fmt.Sprintf("From: %s", req.Email))
|
|||
|
|
|
|||
|
|
// Return success
|
|||
|
|
w.WriteHeader(http.StatusOK)
|
|||
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|||
|
|
"message": "Message sent successfully",
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7. HTML Form Template
|
|||
|
|
```html
|
|||
|
|
<!-- Contact Form with Security Controls -->
|
|||
|
|
<form hx-post="/api/contact"
|
|||
|
|
hx-trigger="submit"
|
|||
|
|
hx-target="#contact-result"
|
|||
|
|
_="on htmx:afterRequest
|
|||
|
|
if event.detail.successful
|
|||
|
|
reset() me
|
|||
|
|
end">
|
|||
|
|
|
|||
|
|
<!-- CSRF Token (hidden) -->
|
|||
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|||
|
|
|
|||
|
|
<!-- Timestamp (for timing validation) -->
|
|||
|
|
<input type="hidden" name="timestamp" value="" id="form-timestamp">
|
|||
|
|
|
|||
|
|
<!-- Honeypot field (hidden from real users, bots will fill it) -->
|
|||
|
|
<input type="text"
|
|||
|
|
name="website"
|
|||
|
|
id="website"
|
|||
|
|
style="position:absolute;left:-9999px;"
|
|||
|
|
tabindex="-1"
|
|||
|
|
autocomplete="off">
|
|||
|
|
|
|||
|
|
<!-- Real fields -->
|
|||
|
|
<div>
|
|||
|
|
<label for="name">Name *</label>
|
|||
|
|
<input type="text"
|
|||
|
|
name="name"
|
|||
|
|
id="name"
|
|||
|
|
required
|
|||
|
|
maxlength="100"
|
|||
|
|
pattern="[a-zA-Z\s'-]+"
|
|||
|
|
title="Name can only contain letters, spaces, hyphens, and apostrophes">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="email">Email *</label>
|
|||
|
|
<input type="email"
|
|||
|
|
name="email"
|
|||
|
|
id="email"
|
|||
|
|
required
|
|||
|
|
maxlength="254"
|
|||
|
|
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
|
|||
|
|
title="Please enter a valid email address">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="company">Company</label>
|
|||
|
|
<input type="text"
|
|||
|
|
name="company"
|
|||
|
|
id="company"
|
|||
|
|
maxlength="100">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="subject">Subject *</label>
|
|||
|
|
<input type="text"
|
|||
|
|
name="subject"
|
|||
|
|
id="subject"
|
|||
|
|
required
|
|||
|
|
maxlength="200"
|
|||
|
|
pattern="[a-zA-Z0-9\s.,!?\-]+"
|
|||
|
|
title="Subject can only contain letters, numbers, spaces, and basic punctuation">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label for="message">Message *</label>
|
|||
|
|
<textarea name="message"
|
|||
|
|
id="message"
|
|||
|
|
required
|
|||
|
|
maxlength="5000"
|
|||
|
|
rows="6"></textarea>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button type="submit">Send Message</button>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<div id="contact-result"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// Set form load timestamp for timing validation
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
document.getElementById('form-timestamp').value = Math.floor(Date.now() / 1000);
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Logging Implementation
|
|||
|
|
|
|||
|
|
### Structured Security Logger
|
|||
|
|
```go
|
|||
|
|
package middleware
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"log"
|
|||
|
|
"net/http"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type SecurityEvent struct {
|
|||
|
|
Timestamp time.Time `json:"timestamp"`
|
|||
|
|
EventType string `json:"event_type"`
|
|||
|
|
Severity string `json:"severity"`
|
|||
|
|
IP string `json:"ip"`
|
|||
|
|
UserAgent string `json:"user_agent"`
|
|||
|
|
Method string `json:"method"`
|
|||
|
|
Path string `json:"path"`
|
|||
|
|
Details string `json:"details"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LogSecurityEvent logs security-related events in structured format
|
|||
|
|
func LogSecurityEvent(eventType string, r *http.Request, details string) {
|
|||
|
|
severity := getSeverity(eventType)
|
|||
|
|
|
|||
|
|
event := SecurityEvent{
|
|||
|
|
Timestamp: time.Now(),
|
|||
|
|
EventType: eventType,
|
|||
|
|
Severity: severity,
|
|||
|
|
IP: getClientIP(r),
|
|||
|
|
UserAgent: r.Header.Get("User-Agent"),
|
|||
|
|
Method: r.Method,
|
|||
|
|
Path: r.URL.Path,
|
|||
|
|
Details: details,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// JSON format for easy parsing by SIEM systems
|
|||
|
|
eventJSON, _ := json.Marshal(event)
|
|||
|
|
log.Printf("[SECURITY] %s", eventJSON)
|
|||
|
|
|
|||
|
|
// Also log to separate security log file in production
|
|||
|
|
if os.Getenv("GO_ENV") == "production" {
|
|||
|
|
logToSecurityFile(eventJSON)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getSeverity(eventType string) string {
|
|||
|
|
switch eventType {
|
|||
|
|
case "BLOCKED", "CSRF_VIOLATION", "ORIGIN_VIOLATION":
|
|||
|
|
return "HIGH"
|
|||
|
|
case "RATE_LIMIT_EXCEEDED", "VALIDATION_FAILED":
|
|||
|
|
return "MEDIUM"
|
|||
|
|
case "CONTACT_FORM_SENT", "PDF_GENERATED":
|
|||
|
|
return "INFO"
|
|||
|
|
default:
|
|||
|
|
return "LOW"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getClientIP(r *http.Request) string {
|
|||
|
|
// Check X-Forwarded-For header (proxy/load balancer)
|
|||
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|||
|
|
return strings.Split(xff, ",")[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check X-Real-IP header
|
|||
|
|
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
|||
|
|
return xri
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fallback to RemoteAddr
|
|||
|
|
return strings.Split(r.RemoteAddr, ":")[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func logToSecurityFile(eventJSON []byte) {
|
|||
|
|
// Append to /var/log/cv-security.log in production
|
|||
|
|
f, err := os.OpenFile("/var/log/cv-security.log",
|
|||
|
|
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("Failed to open security log: %v", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
|
|||
|
|
f.Write(eventJSON)
|
|||
|
|
f.WriteString("\n")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Enhanced Rate Limiter with Logging
|
|||
|
|
```go
|
|||
|
|
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
|
|||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
ip := getClientIP(r)
|
|||
|
|
|
|||
|
|
if !rl.allow(ip) {
|
|||
|
|
// Log rate limit violation
|
|||
|
|
LogSecurityEvent("RATE_LIMIT_EXCEEDED", r,
|
|||
|
|
fmt.Sprintf("IP: %s, Limit: %d/%v", ip, rl.limit, rl.window))
|
|||
|
|
|
|||
|
|
w.Header().Set("Retry-After", "60")
|
|||
|
|
http.Error(w, "Rate limit exceeded. Please try again later.",
|
|||
|
|
http.StatusTooManyRequests)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next.ServeHTTP(w, r)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Enhanced Origin Checker with Logging
|
|||
|
|
```go
|
|||
|
|
func OriginChecker(next http.Handler) http.Handler {
|
|||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
origin := r.Header.Get("Origin")
|
|||
|
|
referer := r.Header.Get("Referer")
|
|||
|
|
|
|||
|
|
// ... existing validation logic ...
|
|||
|
|
|
|||
|
|
if origin != "" && !isAllowedOrigin(origin, allowedOrigins) {
|
|||
|
|
LogSecurityEvent("ORIGIN_VIOLATION", r,
|
|||
|
|
fmt.Sprintf("Blocked origin: %s", origin))
|
|||
|
|
http.Error(w, "Forbidden: External access not allowed",
|
|||
|
|
http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... rest of validation ...
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Headers Enhancement
|
|||
|
|
|
|||
|
|
### Additional Headers to Add
|
|||
|
|
```go
|
|||
|
|
func SecurityHeaders(next http.Handler) http.Handler {
|
|||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
// Existing headers (keep all current headers)
|
|||
|
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
|||
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|||
|
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|||
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|||
|
|
w.Header().Set("Permissions-Policy", "...")
|
|||
|
|
w.Header().Set("Content-Security-Policy", "...")
|
|||
|
|
|
|||
|
|
// NEW: Additional security headers
|
|||
|
|
|
|||
|
|
// Prevent Adobe Flash/PDF from loading external content
|
|||
|
|
w.Header().Set("X-Permitted-Cross-Domain-Policies", "none")
|
|||
|
|
|
|||
|
|
// Cross-Origin isolation
|
|||
|
|
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
|||
|
|
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
|
|||
|
|
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
|
|||
|
|
|
|||
|
|
// Prevent DNS prefetching of external domains
|
|||
|
|
w.Header().Set("X-DNS-Prefetch-Control", "off")
|
|||
|
|
|
|||
|
|
// HSTS (production only) - already implemented ✅
|
|||
|
|
if os.Getenv("GO_ENV") == "production" {
|
|||
|
|
w.Header().Set("Strict-Transport-Security",
|
|||
|
|
"max-age=31536000; includeSubDomains; preload")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next.ServeHTTP(w, r)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Linux Server Hardening Checklist
|
|||
|
|
|
|||
|
|
### System Security
|
|||
|
|
```bash
|
|||
|
|
# 1. Firewall (UFW)
|
|||
|
|
sudo ufw default deny incoming
|
|||
|
|
sudo ufw default allow outgoing
|
|||
|
|
sudo ufw allow 22/tcp # SSH
|
|||
|
|
sudo ufw allow 80/tcp # HTTP
|
|||
|
|
sudo ufw allow 443/tcp # HTTPS
|
|||
|
|
sudo ufw enable
|
|||
|
|
|
|||
|
|
# 2. Fail2ban (Brute-force protection)
|
|||
|
|
sudo apt install fail2ban
|
|||
|
|
sudo systemctl enable fail2ban
|
|||
|
|
sudo systemctl start fail2ban
|
|||
|
|
|
|||
|
|
# Create jail for CV app (detect repeated 403/429 responses)
|
|||
|
|
cat > /etc/fail2ban/jail.d/cv-app.conf << EOF
|
|||
|
|
[cv-app]
|
|||
|
|
enabled = true
|
|||
|
|
port = http,https
|
|||
|
|
filter = cv-app
|
|||
|
|
logpath = /var/log/cv-security.log
|
|||
|
|
maxretry = 10
|
|||
|
|
findtime = 3600
|
|||
|
|
bantime = 86400
|
|||
|
|
EOF
|
|||
|
|
|
|||
|
|
# 3. SSH Hardening
|
|||
|
|
sudo nano /etc/ssh/sshd_config
|
|||
|
|
# Change:
|
|||
|
|
# PermitRootLogin no
|
|||
|
|
# PasswordAuthentication no
|
|||
|
|
# PubkeyAuthentication yes
|
|||
|
|
# Port 2222 (non-standard port)
|
|||
|
|
sudo systemctl restart sshd
|
|||
|
|
|
|||
|
|
# 4. Automatic Security Updates
|
|||
|
|
sudo apt install unattended-upgrades
|
|||
|
|
sudo dpkg-reconfigure -plow unattended-upgrades
|
|||
|
|
|
|||
|
|
# 5. AppArmor (Application sandboxing)
|
|||
|
|
sudo apt install apparmor apparmor-utils
|
|||
|
|
sudo systemctl enable apparmor
|
|||
|
|
sudo systemctl start apparmor
|
|||
|
|
|
|||
|
|
# 6. Kernel Hardening
|
|||
|
|
sudo nano /etc/sysctl.conf
|
|||
|
|
# Add:
|
|||
|
|
# net.ipv4.conf.all.rp_filter = 1
|
|||
|
|
# net.ipv4.conf.default.rp_filter = 1
|
|||
|
|
# net.ipv4.icmp_echo_ignore_all = 1
|
|||
|
|
# net.ipv4.conf.all.accept_redirects = 0
|
|||
|
|
# net.ipv4.conf.all.send_redirects = 0
|
|||
|
|
# net.ipv4.conf.all.accept_source_route = 0
|
|||
|
|
# net.ipv4.tcp_syncookies = 1
|
|||
|
|
# kernel.dmesg_restrict = 1
|
|||
|
|
sudo sysctl -p
|
|||
|
|
|
|||
|
|
# 7. File Permissions
|
|||
|
|
sudo chmod 600 /etc/ssh/sshd_config
|
|||
|
|
sudo chmod 700 ~/.ssh
|
|||
|
|
sudo chmod 600 ~/.ssh/authorized_keys
|
|||
|
|
sudo chmod 755 /opt/cv-app
|
|||
|
|
sudo chmod 500 /opt/cv-app/cv-server # Read + execute only
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Nginx Security Configuration
|
|||
|
|
```nginx
|
|||
|
|
# /etc/nginx/sites-available/cv-app
|
|||
|
|
|
|||
|
|
# Rate limiting zones
|
|||
|
|
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
|||
|
|
limit_req_zone $binary_remote_addr zone=contact:10m rate=5r/h;
|
|||
|
|
limit_req_zone $binary_remote_addr zone=pdf:10m rate=3r/m;
|
|||
|
|
|
|||
|
|
# Connection limiting
|
|||
|
|
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
|||
|
|
|
|||
|
|
server {
|
|||
|
|
listen 443 ssl http2;
|
|||
|
|
server_name juan.andres.morenorub.io;
|
|||
|
|
|
|||
|
|
# SSL Configuration (A+ rating)
|
|||
|
|
ssl_certificate /etc/letsencrypt/live/juan.andres.morenorub.io/fullchain.pem;
|
|||
|
|
ssl_certificate_key /etc/letsencrypt/live/juan.andres.morenorub.io/privkey.pem;
|
|||
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|||
|
|
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
|||
|
|
ssl_prefer_server_ciphers off;
|
|||
|
|
ssl_session_cache shared:SSL:10m;
|
|||
|
|
ssl_session_timeout 10m;
|
|||
|
|
ssl_stapling on;
|
|||
|
|
ssl_stapling_verify on;
|
|||
|
|
ssl_trusted_certificate /etc/letsencrypt/live/juan.andres.morenorub.io/chain.pem;
|
|||
|
|
|
|||
|
|
# OCSP Stapling
|
|||
|
|
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
|||
|
|
resolver_timeout 5s;
|
|||
|
|
|
|||
|
|
# Security Headers (belt-and-suspenders with Go app)
|
|||
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|||
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|||
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|||
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|||
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|||
|
|
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
|||
|
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
|||
|
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
|||
|
|
|
|||
|
|
# CSP (delegated to Go app, but backup here)
|
|||
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net https://matomo.morenorub.io; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.iconify.design https://matomo.morenorub.io; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;
|
|||
|
|
|
|||
|
|
# Hide Nginx version
|
|||
|
|
server_tokens off;
|
|||
|
|
|
|||
|
|
# Connection limits
|
|||
|
|
limit_conn addr 10;
|
|||
|
|
|
|||
|
|
# General rate limiting
|
|||
|
|
limit_req zone=general burst=20 nodelay;
|
|||
|
|
|
|||
|
|
# Proxy to Go application
|
|||
|
|
location / {
|
|||
|
|
proxy_pass http://127.0.0.1:1999;
|
|||
|
|
proxy_http_version 1.1;
|
|||
|
|
proxy_set_header Upgrade $http_upgrade;
|
|||
|
|
proxy_set_header Connection 'upgrade';
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|||
|
|
proxy_cache_bypass $http_upgrade;
|
|||
|
|
|
|||
|
|
# Timeouts
|
|||
|
|
proxy_connect_timeout 60s;
|
|||
|
|
proxy_send_timeout 60s;
|
|||
|
|
proxy_read_timeout 60s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Contact form endpoint - stricter rate limit
|
|||
|
|
location /api/contact {
|
|||
|
|
limit_req zone=contact burst=1 nodelay;
|
|||
|
|
|
|||
|
|
proxy_pass http://127.0.0.1:1999;
|
|||
|
|
proxy_http_version 1.1;
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# PDF endpoint - rate limit
|
|||
|
|
location /export/pdf {
|
|||
|
|
limit_req zone=pdf burst=1 nodelay;
|
|||
|
|
|
|||
|
|
proxy_pass http://127.0.0.1:1999;
|
|||
|
|
proxy_http_version 1.1;
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|||
|
|
|
|||
|
|
# Extended timeout for PDF generation
|
|||
|
|
proxy_read_timeout 120s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Static files with caching
|
|||
|
|
location /static/ {
|
|||
|
|
alias /opt/cv-app/static/;
|
|||
|
|
expires 1d;
|
|||
|
|
add_header Cache-Control "public, immutable";
|
|||
|
|
|
|||
|
|
# Security headers for static files
|
|||
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|||
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Block access to sensitive files
|
|||
|
|
location ~ /\. {
|
|||
|
|
deny all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
location ~ \.(env|git|md|log)$ {
|
|||
|
|
deny all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Access and error logs
|
|||
|
|
access_log /var/log/nginx/cv-app-access.log combined;
|
|||
|
|
error_log /var/log/nginx/cv-app-error.log warn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# HTTP to HTTPS redirect
|
|||
|
|
server {
|
|||
|
|
listen 80;
|
|||
|
|
server_name juan.andres.morenorub.io;
|
|||
|
|
return 301 https://$server_name$request_uri;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Dependency Security
|
|||
|
|
|
|||
|
|
### Go Modules
|
|||
|
|
```bash
|
|||
|
|
# Check for updates
|
|||
|
|
go list -m -u all
|
|||
|
|
|
|||
|
|
# Audit dependencies
|
|||
|
|
go mod verify
|
|||
|
|
|
|||
|
|
# Vulnerability scanning (install govulncheck)
|
|||
|
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|||
|
|
govulncheck ./...
|
|||
|
|
|
|||
|
|
# Update dependencies
|
|||
|
|
go get -u ./...
|
|||
|
|
go mod tidy
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Frontend Dependencies
|
|||
|
|
```bash
|
|||
|
|
# Check CDN resources for updates
|
|||
|
|
# HTMX: https://unpkg.com/htmx.org@latest
|
|||
|
|
# Hyperscript: https://unpkg.com/hyperscript.org@latest
|
|||
|
|
# Iconify: https://cdn.jsdelivr.net/npm/iconify-icon@latest
|
|||
|
|
|
|||
|
|
# Generate SRI hashes
|
|||
|
|
https://www.srihash.org/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Compliance Considerations
|
|||
|
|
|
|||
|
|
### GDPR (General Data Protection Regulation)
|
|||
|
|
- ✅ No personal data collection (current state)
|
|||
|
|
- ⚠️ Contact form will collect: name, email, company, message
|
|||
|
|
- **Requirement:** Privacy policy page
|
|||
|
|
- **Requirement:** Cookie consent banner (if tracking)
|
|||
|
|
- **Requirement:** Data retention policy
|
|||
|
|
- **Requirement:** Right to deletion (email-based request)
|
|||
|
|
|
|||
|
|
### WCAG 2.1 AA (Accessibility)
|
|||
|
|
- ✅ Semantic HTML structure
|
|||
|
|
- ✅ Keyboard navigation support
|
|||
|
|
- ⚠️ Contact form should have proper labels and ARIA attributes
|
|||
|
|
|
|||
|
|
### Browser Security
|
|||
|
|
- ✅ CSP prevents XSS
|
|||
|
|
- ✅ HTTPS enforced (HSTS)
|
|||
|
|
- ✅ Cookies with Secure, HttpOnly, SameSite flags
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Penetration Testing Guide
|
|||
|
|
|
|||
|
|
### Manual Testing Checklist
|
|||
|
|
|
|||
|
|
#### 1. XSS Testing
|
|||
|
|
```bash
|
|||
|
|
# Test name field
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"<script>alert(1)</script>","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: Rejected or escaped
|
|||
|
|
|
|||
|
|
# Test message field
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"message":"<img src=x onerror=alert(1)>"}'
|
|||
|
|
|
|||
|
|
# Expected: HTML tags stripped
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. Email Header Injection
|
|||
|
|
```bash
|
|||
|
|
# Attempt to inject BCC header
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"Test\r\nBcc: attacker@evil.com","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: Rejected (newlines stripped)
|
|||
|
|
|
|||
|
|
# Attempt to inject additional headers
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"subject":"Test\nContent-Type: text/html","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: Rejected
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. Rate Limiting
|
|||
|
|
```bash
|
|||
|
|
# Test contact form rate limit (should allow 5/hour)
|
|||
|
|
for i in {1..6}; do
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"Test","email":"test@test.com","message":"Test"}' &
|
|||
|
|
done
|
|||
|
|
wait
|
|||
|
|
|
|||
|
|
# Expected: 6th request returns 429 Too Many Requests
|
|||
|
|
|
|||
|
|
# Test PDF rate limit (should allow 3/minute)
|
|||
|
|
for i in {1..4}; do
|
|||
|
|
curl "https://juan.andres.morenorub.io/export/pdf?lang=en" -o /dev/null &
|
|||
|
|
done
|
|||
|
|
wait
|
|||
|
|
|
|||
|
|
# Expected: 4th request returns 429
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. Origin Validation
|
|||
|
|
```bash
|
|||
|
|
# Test contact form from curl (should be blocked)
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"Test","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: 403 Forbidden
|
|||
|
|
|
|||
|
|
# Test with fake Origin header
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Origin: https://evil.com" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"Test","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: 403 Forbidden
|
|||
|
|
|
|||
|
|
# Test PDF endpoint from curl
|
|||
|
|
curl "https://juan.andres.morenorub.io/export/pdf?lang=en"
|
|||
|
|
|
|||
|
|
# Expected: 403 Forbidden (in production)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5. CSRF Testing
|
|||
|
|
```bash
|
|||
|
|
# Attempt CSRF attack without token
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-H "Cookie: session=valid_session" \
|
|||
|
|
-d '{"name":"Test","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: 403 Forbidden (missing CSRF token)
|
|||
|
|
|
|||
|
|
# Attempt with invalid token
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-H "Cookie: csrf_token=valid_token" \
|
|||
|
|
-d '{"csrf_token":"invalid_token","name":"Test","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: 403 Forbidden (token mismatch)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6. SQL Injection (Should be N/A)
|
|||
|
|
```bash
|
|||
|
|
# No database, but test input sanitization
|
|||
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"name":"Robert\"; DROP TABLE users; --","email":"test@test.com"}'
|
|||
|
|
|
|||
|
|
# Expected: Rejected or escaped (no SQL execution anyway)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Automated Scanning Tools
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# OWASP ZAP (Zed Attack Proxy)
|
|||
|
|
docker run -t owasp/zap2docker-stable zap-baseline.py \
|
|||
|
|
-t https://juan.andres.morenorub.io
|
|||
|
|
|
|||
|
|
# Nikto web scanner
|
|||
|
|
nikto -h https://juan.andres.morenorub.io
|
|||
|
|
|
|||
|
|
# SSL/TLS testing
|
|||
|
|
testssl.sh --severity HIGH https://juan.andres.morenorub.io
|
|||
|
|
|
|||
|
|
# Security headers check
|
|||
|
|
curl -I https://juan.andres.morenorub.io | grep -E "(X-|Content-Security|Strict-Transport)"
|
|||
|
|
|
|||
|
|
# CSP validator
|
|||
|
|
https://csp-evaluator.withgoogle.com/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Metrics & Monitoring
|
|||
|
|
|
|||
|
|
### Key Metrics to Track
|
|||
|
|
1. **Rate limit violations** - Track IPs hitting rate limits
|
|||
|
|
2. **Origin validation failures** - Detect hotlinking attempts
|
|||
|
|
3. **CSRF validation failures** - Potential attack indicators
|
|||
|
|
4. **Failed form submissions** - Bot detection effectiveness
|
|||
|
|
5. **PDF generation errors** - Potential DoS attempts
|
|||
|
|
6. **Suspicious user agents** - Bot/scraper activity
|
|||
|
|
|
|||
|
|
### Monitoring Setup
|
|||
|
|
```bash
|
|||
|
|
# Security log monitoring (production)
|
|||
|
|
tail -f /var/log/cv-security.log | jq 'select(.severity == "HIGH")'
|
|||
|
|
|
|||
|
|
# Real-time rate limit violations
|
|||
|
|
grep "RATE_LIMIT_EXCEEDED" /var/log/cv-security.log | tail -n 20
|
|||
|
|
|
|||
|
|
# Geographic distribution of blocked requests
|
|||
|
|
grep "BLOCKED" /var/log/cv-security.log | jq -r '.ip' | sort | uniq -c | sort -rn
|
|||
|
|
|
|||
|
|
# Top blocked user agents
|
|||
|
|
grep "BLOCKED" /var/log/cv-security.log | jq -r '.user_agent' | sort | uniq -c | sort -rn
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Alerting Rules (for Prometheus/Grafana)
|
|||
|
|
```yaml
|
|||
|
|
# Alert on high rate limit violations
|
|||
|
|
- alert: HighRateLimitViolations
|
|||
|
|
expr: rate(cv_rate_limit_violations_total[5m]) > 10
|
|||
|
|
for: 5m
|
|||
|
|
labels:
|
|||
|
|
severity: warning
|
|||
|
|
annotations:
|
|||
|
|
summary: "High rate limit violations detected"
|
|||
|
|
|
|||
|
|
# Alert on CSRF attacks
|
|||
|
|
- alert: CSRFAttackDetected
|
|||
|
|
expr: increase(cv_csrf_violations_total[1h]) > 5
|
|||
|
|
for: 1m
|
|||
|
|
labels:
|
|||
|
|
severity: critical
|
|||
|
|
annotations:
|
|||
|
|
summary: "CSRF attack detected"
|
|||
|
|
|
|||
|
|
# Alert on origin validation failures
|
|||
|
|
- alert: HotlinkingAttempt
|
|||
|
|
expr: rate(cv_origin_violations_total[5m]) > 5
|
|||
|
|
for: 5m
|
|||
|
|
labels:
|
|||
|
|
severity: medium
|
|||
|
|
annotations:
|
|||
|
|
summary: "Potential hotlinking attack detected"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Incident Response Playbook
|
|||
|
|
|
|||
|
|
### 1. Rate Limit Attack (DoS)
|
|||
|
|
**Indicators:**
|
|||
|
|
- Spike in 429 responses
|
|||
|
|
- Single IP hitting rate limits repeatedly
|
|||
|
|
|
|||
|
|
**Response:**
|
|||
|
|
1. Identify attacking IP: `grep "RATE_LIMIT_EXCEEDED" /var/log/cv-security.log | tail -n 100`
|
|||
|
|
2. Ban IP with fail2ban: `sudo fail2ban-client set cv-app banip <IP>`
|
|||
|
|
3. Review logs for patterns
|
|||
|
|
4. Consider lowering rate limits temporarily
|
|||
|
|
|
|||
|
|
### 2. CSRF Attack
|
|||
|
|
**Indicators:**
|
|||
|
|
- Multiple CSRF validation failures
|
|||
|
|
- Attempts from different IPs with same pattern
|
|||
|
|
|
|||
|
|
**Response:**
|
|||
|
|
1. Rotate CSRF secret
|
|||
|
|
2. Review user sessions
|
|||
|
|
3. Check for XSS vulnerability that could steal tokens
|
|||
|
|
4. Increase logging verbosity
|
|||
|
|
|
|||
|
|
### 3. Email Header Injection Attempt
|
|||
|
|
**Indicators:**
|
|||
|
|
- Contact form submissions with newlines in headers
|
|||
|
|
- Failed validation for email fields
|
|||
|
|
|
|||
|
|
**Response:**
|
|||
|
|
1. Verify sanitization is working
|
|||
|
|
2. Check email logs for suspicious sends
|
|||
|
|
3. Review all contact form submissions from that IP
|
|||
|
|
4. Ban IP if repeated attempts
|
|||
|
|
|
|||
|
|
### 4. Brute Force Attack
|
|||
|
|
**Indicators:**
|
|||
|
|
- Repeated failed requests from same IP
|
|||
|
|
- Multiple POST requests in short time
|
|||
|
|
|
|||
|
|
**Response:**
|
|||
|
|
1. Verify rate limiting is active
|
|||
|
|
2. Ban IP with fail2ban
|
|||
|
|
3. Review user agents (might be bot network)
|
|||
|
|
4. Consider CAPTCHA if persistent
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Testing Schedule
|
|||
|
|
|
|||
|
|
### Daily
|
|||
|
|
- ✅ Review security logs for anomalies
|
|||
|
|
- ✅ Check fail2ban banned IPs
|
|||
|
|
|
|||
|
|
### Weekly
|
|||
|
|
- ✅ Run `govulncheck` for dependency vulnerabilities
|
|||
|
|
- ✅ Review rate limit violations
|
|||
|
|
- ✅ Check SSL certificate expiry
|
|||
|
|
|
|||
|
|
### Monthly
|
|||
|
|
- ✅ Update Go dependencies
|
|||
|
|
- ✅ Run full OWASP ZAP scan
|
|||
|
|
- ✅ Review and rotate logs
|
|||
|
|
- ✅ Test backup/restore procedures
|
|||
|
|
|
|||
|
|
### Quarterly
|
|||
|
|
- ✅ Full penetration test (manual + automated)
|
|||
|
|
- ✅ Security audit review
|
|||
|
|
- ✅ Update security policies
|
|||
|
|
- ✅ Review and update rate limits based on traffic
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Summary of Required Actions
|
|||
|
|
|
|||
|
|
### CRITICAL (Implement before contact form goes live)
|
|||
|
|
1. ✅ **Origin Validation for Contact Form** - Browser-only access
|
|||
|
|
2. ✅ **CSRF Token System** - Generate, validate, rotate
|
|||
|
|
3. ✅ **Input Validation** - Email, name, subject, message sanitization
|
|||
|
|
4. ✅ **Email Header Injection Prevention** - Strip newlines, validate headers
|
|||
|
|
5. ✅ **Rate Limiting** - 5 requests/hour for contact endpoint
|
|||
|
|
6. ✅ **Bot Protection** - Honeypot + timing validation
|
|||
|
|
7. ✅ **Security Logging** - Track all security events
|
|||
|
|
|
|||
|
|
### HIGH PRIORITY
|
|||
|
|
1. ⚠️ Add SRI hashes for Hyperscript and Iconify
|
|||
|
|
2. ⚠️ Implement structured security logging
|
|||
|
|
3. ⚠️ Set up fail2ban for repeated attacks
|
|||
|
|
4. ⚠️ Configure Nginx with security headers and rate limits
|
|||
|
|
|
|||
|
|
### MEDIUM PRIORITY
|
|||
|
|
1. ⚠️ Add additional security headers (X-Permitted-Cross-Domain-Policies, etc.)
|
|||
|
|
2. ⚠️ Implement automated dependency scanning
|
|||
|
|
3. ⚠️ Set up security monitoring dashboard
|
|||
|
|
4. ⚠️ Create privacy policy for GDPR compliance
|
|||
|
|
|
|||
|
|
### LOW PRIORITY (Nice to have)
|
|||
|
|
1. ℹ️ CSP nonce-based script loading (instead of 'unsafe-inline')
|
|||
|
|
2. ℹ️ Security bug bounty program
|
|||
|
|
3. ℹ️ Penetration testing by third party
|
|||
|
|
4. ℹ️ SOC 2 Type II compliance (if needed for clients)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Risk Matrix
|
|||
|
|
|
|||
|
|
| Risk | Likelihood | Impact | Severity | Mitigation |
|
|||
|
|
|------|-----------|---------|----------|------------|
|
|||
|
|
| XSS Attack | Low | High | Medium | ✅ html/template auto-escaping |
|
|||
|
|
| CSRF Attack | Medium | Low | Low | ⚠️ Implement tokens |
|
|||
|
|
| Rate Limit DoS | Medium | Medium | Medium | ✅ Rate limiting active |
|
|||
|
|
| Email Header Injection | Low | High | Medium | ⚠️ Implement sanitization |
|
|||
|
|
| SQL Injection | N/A | N/A | N/A | ✅ No database |
|
|||
|
|
| Command Injection | Very Low | Critical | Low | ✅ go-git library used |
|
|||
|
|
| DDoS Attack | Medium | Medium | Medium | ⚠️ Cloudflare/rate limiting |
|
|||
|
|
| Brute Force | Low | Low | Low | ✅ Rate limiting |
|
|||
|
|
| Path Traversal | Very Low | High | Low | ✅ Validation implemented |
|
|||
|
|
| Dependency Vuln | Medium | Medium | Medium | ⚠️ Regular updates needed |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
The CV application demonstrates **strong security fundamentals** with proper XSS prevention, command injection mitigation, and comprehensive security headers. The main areas requiring attention are:
|
|||
|
|
|
|||
|
|
1. **CSRF protection** for POST endpoints (low risk, but should implement)
|
|||
|
|
2. **Security logging** for attack detection and incident response
|
|||
|
|
3. **Contact form security** (when implemented) with strict validation and origin checks
|
|||
|
|
|
|||
|
|
**Overall Security Rating: B+ (GOOD)**
|
|||
|
|
|
|||
|
|
With the recommended improvements, the application can achieve an **A+ security rating** and be confidently opened to public hacking challenges.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Next Steps:**
|
|||
|
|
1. Review this report with the development team
|
|||
|
|
2. Implement contact form with security controls (see design above)
|
|||
|
|
3. Add CSRF protection to existing POST endpoints
|
|||
|
|
4. Set up security logging and monitoring
|
|||
|
|
5. Test all security controls with penetration testing tools
|
|||
|
|
6. Deploy to production with Nginx security configuration
|
|||
|
|
7. Monitor security logs and iterate on defenses
|
|||
|
|
|
|||
|
|
**Security is a continuous process, not a destination. Regular audits, updates, and monitoring are essential.**
|