- 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
47 KiB
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/templatewith auto-escaping - ✅ Command Injection Prevention - Uses
go-gitlibrary 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
/internal/middleware/security.go- Security headers, rate limiting, origin validation/internal/middleware/logger.go- Request logging/internal/middleware/recovery.go- Panic recovery/internal/handlers/*.go- All HTTP handlers/internal/templates/template.go- Template rendering/internal/pdf/generator.go- PDF generation/internal/routes/routes.go- Routing configuration/main.go- Server initialization/templates/*.html- All HTML templates
Security Test Files
/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
.envfile (not committed to git) - ✅
.env.exampleprovided 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:
- Implement CSRF token generation and validation
- Add token to all POST requests via HTMX
- 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:
- ✅ CSP is comprehensive
- ⚠️ Consider tightening
'unsafe-inline'for scripts (use nonces) - ✅ HSTS properly configured for production
- ⚠️ Add
X-Permitted-Cross-Domain-Policies: none - ⚠️ Add
Cross-Origin-Opener-Policy: same-origin - ⚠️ 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:
- Run
go list -m -u allregularly for updates - Subscribe to security advisories for:
- chromedp (Chromium vulnerabilities)
- go-git (Git parsing vulnerabilities)
- 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/templateauto-escapes all variables - ✅
safeHTMLonly used for trusted CV content - ✅ No user input rendered without escaping
- ✅ CSP blocks inline scripts (except whitelisted CDNs)
2. Command Injection Prevention
- ✅ Uses
go-gitlibrary instead ofexec.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:
- Email header injection prevention
- Strict input validation (see design below)
- Rate limiting (5 requests/hour per IP)
- Bot protection (honeypot + timing)
- Origin validation (BROWSER-ONLY access)
- CSRF token validation
- Email address validation (RFC 5322)
- 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
- Rate limit violations - Track IPs hitting rate limits
- Origin validation failures - Detect hotlinking attempts
- CSRF validation failures - Potential attack indicators
- Failed form submissions - Bot detection effectiveness
- PDF generation errors - Potential DoS attempts
- 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:
- Identify attacking IP:
grep "RATE_LIMIT_EXCEEDED" /var/log/cv-security.log | tail -n 100 - Ban IP with fail2ban:
sudo fail2ban-client set cv-app banip <IP> - Review logs for patterns
- Consider lowering rate limits temporarily
2. CSRF Attack
Indicators:
- Multiple CSRF validation failures
- Attempts from different IPs with same pattern
Response:
- Rotate CSRF secret
- Review user sessions
- Check for XSS vulnerability that could steal tokens
- Increase logging verbosity
3. Email Header Injection Attempt
Indicators:
- Contact form submissions with newlines in headers
- Failed validation for email fields
Response:
- Verify sanitization is working
- Check email logs for suspicious sends
- Review all contact form submissions from that IP
- Ban IP if repeated attempts
4. Brute Force Attack
Indicators:
- Repeated failed requests from same IP
- Multiple POST requests in short time
Response:
- Verify rate limiting is active
- Ban IP with fail2ban
- Review user agents (might be bot network)
- Consider CAPTCHA if persistent
Security Testing Schedule
Daily
- ✅ Review security logs for anomalies
- ✅ Check fail2ban banned IPs
Weekly
- ✅ Run
govulncheckfor 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)
- ✅ Origin Validation for Contact Form - Browser-only access
- ✅ CSRF Token System - Generate, validate, rotate
- ✅ Input Validation - Email, name, subject, message sanitization
- ✅ Email Header Injection Prevention - Strip newlines, validate headers
- ✅ Rate Limiting - 5 requests/hour for contact endpoint
- ✅ Bot Protection - Honeypot + timing validation
- ✅ Security Logging - Track all security events
HIGH PRIORITY
- ⚠️ Add SRI hashes for Hyperscript and Iconify
- ⚠️ Implement structured security logging
- ⚠️ Set up fail2ban for repeated attacks
- ⚠️ Configure Nginx with security headers and rate limits
MEDIUM PRIORITY
- ⚠️ Add additional security headers (X-Permitted-Cross-Domain-Policies, etc.)
- ⚠️ Implement automated dependency scanning
- ⚠️ Set up security monitoring dashboard
- ⚠️ Create privacy policy for GDPR compliance
LOW PRIORITY (Nice to have)
- ℹ️ CSP nonce-based script loading (instead of 'unsafe-inline')
- ℹ️ Security bug bounty program
- ℹ️ Penetration testing by third party
- ℹ️ 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:
- CSRF protection for POST endpoints (low risk, but should implement)
- Security logging for attack detection and incident response
- 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:
- Review this report with the development team
- Implement contact form with security controls (see design above)
- Add CSRF protection to existing POST endpoints
- Set up security logging and monitoring
- Test all security controls with penetration testing tools
- Deploy to production with Nginx security configuration
- Monitor security logs and iterate on defenses
Security is a continuous process, not a destination. Regular audits, updates, and monitoring are essential.