# 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 ``` **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
``` --- ## 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":"","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":"