Files
cv-site/doc/18-SECURITY-AUDIT.md
T
juanatsap 8f4d0e9433 feat: self-host HTMX 2.0.10 and Hyperscript 0.9.91, remove unpkg CDN
- 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
2026-05-14 12:59:30 +01:00

47 KiB
Raw Blame History

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:

// 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:

✅ 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's html/template automatically escapes all variables
{{.CV.Personal.Name}}  // Auto-escaped
{{.CV.Personal.Email}} // Auto-escaped

SafeHTML Usage - CONTROLLED:

// 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:

// Uses pure Go library, NO shell commands
repo, err := git.PlainOpen(repoPath)
// Instead of: exec.Command("git", "log", repoPath)

Security Tests:

✅ 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:

// 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):

// 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.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:

// 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:

<!-- 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:

// 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:

// 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)

// 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

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)

// 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

// 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

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

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

<!-- 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

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

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

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

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

# 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

# /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

# 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

# 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

# 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

# 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

# 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

# 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

# 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)

# 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

# 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

# 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)

# 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.