Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
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://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:
- ✅ 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.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:
<!-- Add SRI hashes -->
<script src="https://unpkg.com/hyperscript.org@0.9.14"
integrity="sha384-[GENERATE_SRI_HASH]"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"
integrity="sha384-[GENERATE_SRI_HASH]"
crossorigin="anonymous"></script>
Generate SRI: https://www.srihash.org/
A09: Security Logging and Monitoring ⚠️ NEEDS IMPROVEMENT
Status: Basic logging, needs security event tracking
Current Logging:
// 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://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
# 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
# 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
# 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.