8f4d0e9433
- Download htmx.min.js v2.0.10 and _hyperscript.min.js v0.9.91 locally - Update head-scripts.html to load from /static/ instead of unpkg CDN - Remove https://unpkg.com from CSP script-src whitelist - Update all documentation references to reflect self-hosted paths - No breaking changes: all hx-* attributes are HTMX 2.0 compatible
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://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 2.0.10 (self-hosted at /static/htmx/htmx.min.js)
|
||
hyperscript 0.9.91 (self-hosted at /static/hyperscript/_hyperscript.min.js)
|
||
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
|
||
<!-- HTMX and Hyperscript are now self-hosted (no SRI needed) -->
|
||
<script src="/static/htmx/htmx.min.js"></script>
|
||
<script src="/static/hyperscript/_hyperscript.min.js"></script>
|
||
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://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
|
||
# HTMX and Hyperscript are self-hosted (update by downloading new versions)
|
||
# HTMX: static/htmx/htmx.min.js (currently 2.0.10)
|
||
# Hyperscript: static/hyperscript/_hyperscript.min.js (currently 0.9.91)
|
||
# Iconify (CDN): 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**.
|
||
|
||
---
|
||
|
||
**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.**
|