Files
cv-site/doc/18-SECURITY-AUDIT.md
T

1595 lines
47 KiB
Markdown
Raw Normal View 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:**
```go
// Origin validation implemented
func OriginChecker(next http.Handler) http.Handler {
// Validates Origin and Referer headers
// Blocks external access to resource-intensive endpoints
}
// Path traversal prevention
func validateRepoPath(path string) error {
// Ensures path is within project directory
// Prevents ../../../etc/passwd attacks
}
```
**Test Results:**
```bash
✅ PASS: TestValidateRepoPath (path traversal prevention)
✅ PASS: TestGetGitRepoFirstCommitDate_SecurityValidation
```
---
### A02: Cryptographic Failures ✅ SECURE
**Status:** No sensitive data storage, proper TLS configuration
**Current Controls:**
- HSTS header enforced in production (1 year, includeSubDomains, preload)
- No passwords, API keys, or secrets in codebase
- Environment variables used for configuration
- TLS termination recommended at reverse proxy (Nginx)
**Recommendations:**
- ✅ Already using `.env` file (not committed to git)
-`.env.example` provided without secrets
- ⚠️ Ensure production uses strong TLS cipher suites (see Nginx config below)
---
### A03: Injection ✅ SECURE
**Status:** No injection vulnerabilities found
#### SQL Injection: N/A
- **No database usage** - Application reads from JSON files only
- Static data in `/data/cv-{lang}.json`
#### XSS (Cross-Site Scripting): SECURE ✅
**Template Auto-Escaping:**
```go
// Go's html/template automatically escapes all variables
{{.CV.Personal.Name}} // Auto-escaped
{{.CV.Personal.Email}} // Auto-escaped
```
**SafeHTML Usage - CONTROLLED:**
```go
// Only used for trusted CV YAML content, never user input
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
}
```
**Verification:**
- All user-facing data passes through `html/template`
- No `innerHTML`, `eval()`, or dangerous DOM manipulation
- CSP header blocks inline scripts (except trusted sources)
#### Command Injection: SECURE ✅
**go-git Library Usage:**
```go
// Uses pure Go library, NO shell commands
repo, err := git.PlainOpen(repoPath)
// Instead of: exec.Command("git", "log", repoPath)
```
**Security Tests:**
```bash
✅ Malicious paths rejected: "../../../etc/passwd", "data | cat /etc/passwd"
✅ Command injection attempts blocked: "data; rm -rf /", "data`whoami`"
```
---
### A04: Insecure Design ⚠️ NEEDS IMPROVEMENT
**Status:** Generally secure, CSRF protection needed
**Current State:**
- ✅ Rate limiting on resource-intensive endpoints
- ✅ Origin validation prevents hotlinking
- ⚠️ **MISSING:** CSRF tokens for POST endpoints
- ⚠️ **MISSING:** Security event logging
**CSRF Vulnerability - POST Endpoints:**
```go
// VULNERABLE: No CSRF protection
POST /toggle/length
POST /toggle/icons
POST /toggle/theme
POST /switch-language
```
**Impact:** Low (only changes user preferences, no data modification)
**Recommended Fix:**
1. Implement CSRF token generation and validation
2. Add token to all POST requests via HTMX
3. Validate token in middleware
---
### A05: Security Misconfiguration ✅ MOSTLY SECURE
**Status:** Good security headers, minor improvements needed
**Current Security Headers (Excellent):**
```go
// Strong CSP policy
Content-Security-Policy: default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
...
// Clickjacking protection
X-Frame-Options: SAMEORIGIN
// MIME sniffing prevention
X-Content-Type-Options: nosniff
// HSTS (production only)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// Privacy protection
Referrer-Policy: strict-origin-when-cross-origin
// Feature policy
Permissions-Policy: geolocation=(), microphone=(), camera=(), ...
```
**Recommendations:**
1. ✅ CSP is comprehensive
2. ⚠️ Consider tightening `'unsafe-inline'` for scripts (use nonces)
3. ✅ HSTS properly configured for production
4. ⚠️ Add `X-Permitted-Cross-Domain-Policies: none`
5. ⚠️ Add `Cross-Origin-Opener-Policy: same-origin`
6. ⚠️ Add `Cross-Origin-Embedder-Policy: require-corp`
---
### A06: Vulnerable and Outdated Components ⚠️ CHECK REGULARLY
**Status:** Dependencies need regular auditing
**Current Dependencies:**
```go
// go.mod
chromedp/chromedp v0.14.2 // PDF generation
joho/godotenv v1.5.1 // Environment variables
go-git/go-git v5.16.4 // Git operations (no shell commands)
```
**Recommendations:**
1. Run `go list -m -u all` regularly for updates
2. Subscribe to security advisories for:
- chromedp (Chromium vulnerabilities)
- go-git (Git parsing vulnerabilities)
3. Implement automated dependency scanning (Dependabot/Snyk)
**Frontend Dependencies:**
```javascript
// index.html - Using CDN with SRI
htmx 2.0.10 (self-hosted at /static/htmx/htmx.min.js)
hyperscript 0.9.91 (self-hosted at /static/hyperscript/_hyperscript.min.js)
iconify-icon@2.1.0 (no SRI - ADD THIS)
```
**Action Items:**
- ⚠️ Add SRI hashes for all CDN scripts
- ⚠️ Update hyperscript to latest version
- ⚠️ Monitor HTMX security advisories
---
### A07: Identification and Authentication Failures N/A
**Status:** No authentication system (public portfolio)
**Rationale:**
- Public CV portfolio site - no login required
- No user accounts or sessions
- Cookies only store UI preferences (non-sensitive)
**Future Contact Form:**
- Will require email validation
- Rate limiting per IP (5/hour recommended)
- Honeypot + timing validation for bot prevention
---
### A08: Software and Data Integrity Failures ⚠️ NEEDS IMPROVEMENT
**Status:** Missing SRI for some CDN resources
**Current State:**
- ✅ HTMX loaded with SRI hash
- ⚠️ Hyperscript missing SRI
- ⚠️ Iconify missing SRI
- ✅ No code integrity checks (not needed for static Go binary)
**Recommendations:**
```html
<!-- HTMX and Hyperscript are now self-hosted (no SRI needed) -->
<script src="/static/htmx/htmx.min.js"></script>
<script src="/static/hyperscript/_hyperscript.min.js"></script>
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"
integrity="sha384-[GENERATE_SRI_HASH]"
crossorigin="anonymous"></script>
```
**Generate SRI:** https://www.srihash.org/
---
### A09: Security Logging and Monitoring ⚠️ NEEDS IMPROVEMENT
**Status:** Basic logging, needs security event tracking
**Current Logging:**
```go
// Basic request logging
log.Printf("[%s] %s %s - %d (%v)", r.Method, r.URL.Path, r.RemoteAddr, status, duration)
// Error logging
log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, err)
```
**Missing Security Events:**
- ❌ Rate limit violations
- ❌ Origin validation failures
- ❌ CSRF validation failures (when implemented)
- ❌ Suspicious request patterns
- ❌ PDF generation failures (could indicate attack)
**Recommendations:** See Security Logging section below
---
### A10: Server-Side Request Forgery (SSRF) ✅ SECURE
**Status:** No SSRF vulnerability
**Analysis:**
- PDF generation uses internal server address only
- No user-controlled URLs in `chromedp.Navigate()`
- No external HTTP requests from user input
**Current Implementation:**
```go
// Hardcoded server address, not user-controlled
targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, req.Lang)
pdfData, err := h.pdfGenerator.GenerateFromURLWithOptions(ctx, targetURL, cookies, renderMode)
```
**No SSRF risk - URL is constructed server-side with validated parameters only.**
---
## Security Strengths 💪
### 1. Template Security (XSS Prevention)
- ✅ Go's `html/template` auto-escapes all variables
-`safeHTML` only used for trusted CV content
- ✅ No user input rendered without escaping
- ✅ CSP blocks inline scripts (except whitelisted CDNs)
### 2. Command Injection Prevention
- ✅ Uses `go-git` library instead of `exec.Command`
- ✅ Path traversal prevention with `validateRepoPath()`
- ✅ Comprehensive security tests for malicious paths
### 3. Security Headers (Best-in-Class)
- ✅ Content Security Policy (CSP)
- ✅ HTTP Strict Transport Security (HSTS)
- ✅ X-Frame-Options (clickjacking prevention)
- ✅ X-Content-Type-Options (MIME sniffing prevention)
- ✅ Referrer-Policy (privacy)
- ✅ Permissions-Policy (feature restrictions)
### 4. Rate Limiting
- ✅ Implemented on PDF endpoint (3 requests/minute)
- ✅ In-memory rate limiter with automatic cleanup
- ✅ Proper 429 Too Many Requests response
### 5. Origin Validation
- ✅ Prevents external hotlinking of PDF endpoint
- ✅ Validates Origin and Referer headers
- ✅ Configurable via ALLOWED_ORIGINS env variable
### 6. Input Validation
- ✅ Strict validation for PDF export parameters
- ✅ Language: only "en" or "es" allowed
- ✅ Length, icons, version parameters validated
- ✅ Rejects invalid inputs with proper error messages
---
## Security Weaknesses & Recommendations
### 1. CSRF Protection - MISSING ⚠️
**Priority:** Medium
**Severity:** Low (no sensitive data, only UI preferences)
**Vulnerable Endpoints:**
```
POST /toggle/length
POST /toggle/icons
POST /toggle/theme
POST /switch-language
```
**Recommendation:** Implement CSRF token system (see implementation below)
---
### 2. Security Logging - INSUFFICIENT ⚠️
**Priority:** High
**Severity:** Medium
**Missing Events:**
- Rate limit violations (potential attack indicators)
- Origin validation failures (hotlinking attempts)
- Repeated failed requests (reconnaissance/scanning)
- Suspicious user agents (bots, scrapers)
**Recommendation:** Implement structured security logging (see implementation below)
---
### 3. Subresource Integrity - INCOMPLETE ⚠️
**Priority:** Medium
**Severity:** Low
**Missing SRI Hashes:**
- Hyperscript library
- Iconify library
**Recommendation:** Add SRI hashes for all CDN resources
---
### 4. Input Sanitization for Future Contact Form ⚠️
**Priority:** CRITICAL (when implementing contact form)
**Required Security Controls:**
1. Email header injection prevention
2. Strict input validation (see design below)
3. Rate limiting (5 requests/hour per IP)
4. Bot protection (honeypot + timing)
5. **Origin validation (BROWSER-ONLY access)**
6. CSRF token validation
7. Email address validation (RFC 5322)
8. Content-Type enforcement
**See Contact Form Security Design below for complete implementation.**
---
## Contact Form Security Design 🔐
### CRITICAL REQUIREMENT: Browser-Only Access
The contact form endpoint MUST reject all non-browser requests:
- ❌ Block: curl, Postman, wget, Python requests, HTTPie, etc.
- ✅ Allow: Only genuine browser requests from the same origin
### Security Architecture
```
Browser → [Origin Validation] → [CSRF Token] → [Rate Limit] →
→ [Bot Detection] → [Input Validation] → [Email Sanitization] → [Send Email]
```
### 1. Origin Validation (CRITICAL)
```go
// BrowserOnlyMiddleware - Blocks non-browser requests
func BrowserOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check 1: Require Origin or Referer header
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")
if origin == "" && referer == "" {
// No Origin/Referer = command-line tool (curl, wget)
logSecurityEvent("BLOCKED", r, "Missing Origin/Referer headers")
http.Error(w, "Forbidden: Browser access required", http.StatusForbidden)
return
}
// Check 2: Validate origin matches allowed domains
if !isAllowedOrigin(origin, allowedOrigins) && !isAllowedOrigin(referer, allowedOrigins) {
logSecurityEvent("BLOCKED", r, "Invalid origin: " + origin)
http.Error(w, "Forbidden: Invalid origin", http.StatusForbidden)
return
}
// Check 3: Require X-Requested-With header (AJAX/HTMX)
requestedWith := r.Header.Get("X-Requested-With")
if requestedWith != "XMLHttpRequest" && r.Header.Get("HX-Request") == "" {
logSecurityEvent("BLOCKED", r, "Missing X-Requested-With/HX-Request")
http.Error(w, "Forbidden: AJAX/HTMX request required", http.StatusForbidden)
return
}
// Check 4: User-Agent validation (reject obvious bots)
ua := r.Header.Get("User-Agent")
if isSuspiciousUserAgent(ua) {
logSecurityEvent("BLOCKED", r, "Suspicious User-Agent: " + ua)
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func isSuspiciousUserAgent(ua string) bool {
ua = strings.ToLower(ua)
// Block common command-line tools
blockedAgents := []string{
"curl", "wget", "python-requests", "postman",
"httpie", "insomnia", "paw", "rest-client",
"apache-httpclient", "java/", "go-http-client",
"libwww-perl", "axios", "node-fetch",
}
for _, blocked := range blockedAgents {
if strings.Contains(ua, blocked) {
return true
}
}
// Empty User-Agent (common for scripts)
if ua == "" {
return true
}
return false
}
```
### 2. Input Validation
```go
type ContactFormRequest struct {
Name string `json:"name" validate:"required,max=100,alpha_space"`
Email string `json:"email" validate:"required,email,max=254"`
Company string `json:"company" validate:"max=100,alphanum_space"`
Subject string `json:"subject" validate:"required,max=200,safe_chars"`
Message string `json:"message" validate:"required,max=5000"`
// Bot detection fields
Honeypot string `json:"website"` // Should be empty
Timestamp int64 `json:"timestamp"` // Form load time
}
// ValidateContactForm performs comprehensive validation
func ValidateContactForm(req *ContactFormRequest) error {
// Honeypot check
if req.Honeypot != "" {
return errors.New("bot detected")
}
// Timing check (must take at least 2 seconds)
now := time.Now().Unix()
if now - req.Timestamp < 2 {
return errors.New("form submitted too quickly")
}
// Email validation (RFC 5322)
if !isValidEmail(req.Email) {
return errors.New("invalid email address")
}
// Email header injection prevention
if containsEmailInjection(req.Name) || containsEmailInjection(req.Email) ||
containsEmailInjection(req.Subject) {
return errors.New("invalid characters in email fields")
}
// Name validation (alphanumeric + spaces only)
if !regexp.MustCompile(`^[a-zA-Z\s'-]+$`).MatchString(req.Name) {
return errors.New("invalid name format")
}
// Subject validation (safe characters only)
if !regexp.MustCompile(`^[a-zA-Z0-9\s.,!?-]+$`).MatchString(req.Subject) {
return errors.New("invalid subject format")
}
// Message sanitization (strip HTML tags)
req.Message = stripHTMLTags(req.Message)
return nil
}
// containsEmailInjection checks for email header injection attempts
func containsEmailInjection(s string) bool {
// Check for newlines (header injection)
if strings.ContainsAny(s, "\r\n") {
return true
}
// Check for email header patterns
dangerousPatterns := []string{
"Content-Type:", "MIME-Version:", "Content-Transfer-Encoding:",
"bcc:", "cc:", "to:", "from:",
}
sLower := strings.ToLower(s)
for _, pattern := range dangerousPatterns {
if strings.Contains(sLower, pattern) {
return true
}
}
return false
}
func isValidEmail(email string) bool {
// RFC 5322 regex (simplified)
pattern := `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched && len(email) <= 254
}
func stripHTMLTags(s string) string {
// Remove all HTML tags
re := regexp.MustCompile(`<[^>]*>`)
return re.ReplaceAllString(s, "")
}
```
### 3. Rate Limiting (Strict)
```go
// Contact form rate limiter: 5 requests per hour per IP
contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)
// Apply to contact endpoint
protectedContactHandler := middleware.BrowserOnlyMiddleware(
middleware.CSRFProtection(
contactRateLimiter.Middleware(
http.HandlerFunc(contactHandler.SendMessage),
),
),
)
mux.Handle("/api/contact", protectedContactHandler)
```
### 4. CSRF Protection
```go
// Generate CSRF token on page load
func (h *ContactHandler) ShowContactForm(w http.ResponseWriter, r *http.Request) {
token := generateCSRFToken()
// Set token in secure cookie
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode,
MaxAge: 3600, // 1 hour
})
// Also pass token to template for hidden field
data := map[string]interface{}{
"CSRFToken": token,
}
h.templates.Render(w, "contact.html", data)
}
// Validate CSRF token on submission
func validateCSRFToken(r *http.Request) error {
cookie, err := r.Cookie("csrf_token")
if err != nil {
return errors.New("missing CSRF token cookie")
}
formToken := r.FormValue("csrf_token")
if formToken == "" {
return errors.New("missing CSRF token in request")
}
if !secureCompare(cookie.Value, formToken) {
return errors.New("CSRF token mismatch")
}
return nil
}
// Constant-time comparison to prevent timing attacks
func secureCompare(a, b string) bool {
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
func generateCSRFToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
```
### 5. Email Sanitization
```go
func sanitizeEmailContent(req *ContactFormRequest) {
// Remove all newlines from header fields
req.Name = strings.ReplaceAll(req.Name, "\r", "")
req.Name = strings.ReplaceAll(req.Name, "\n", "")
req.Email = strings.ReplaceAll(req.Email, "\r", "")
req.Email = strings.ReplaceAll(req.Email, "\n", "")
req.Subject = strings.ReplaceAll(req.Subject, "\r", "")
req.Subject = strings.ReplaceAll(req.Subject, "\n", "")
// Trim whitespace
req.Name = strings.TrimSpace(req.Name)
req.Email = strings.TrimSpace(req.Email)
req.Company = strings.TrimSpace(req.Company)
req.Subject = strings.TrimSpace(req.Subject)
req.Message = strings.TrimSpace(req.Message)
// HTML entity encoding for message body
req.Message = template.HTMLEscapeString(req.Message)
}
```
### 6. Complete Contact Handler
```go
func (h *ContactHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
// Parse request
var req ContactFormRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
HandleError(w, r, BadRequestError("Invalid request format"))
return
}
// Set timestamp from server (don't trust client)
req.Timestamp = time.Now().Unix()
// Validate CSRF token
if err := validateCSRFToken(r); err != nil {
logSecurityEvent("CSRF_VIOLATION", r, err.Error())
HandleError(w, r, ForbiddenError("CSRF validation failed"))
return
}
// Validate input
if err := ValidateContactForm(&req); err != nil {
logSecurityEvent("VALIDATION_FAILED", r, err.Error())
HandleError(w, r, BadRequestError(err.Error()))
return
}
// Sanitize content
sanitizeEmailContent(&req)
// Send email (using standard library or third-party service)
if err := h.emailService.Send(req); err != nil {
logSecurityEvent("EMAIL_SEND_FAILED", r, err.Error())
HandleError(w, r, InternalError(err))
return
}
// Log success
logSecurityEvent("CONTACT_FORM_SENT", r, fmt.Sprintf("From: %s", req.Email))
// Return success
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"message": "Message sent successfully",
})
}
```
### 7. HTML Form Template
```html
<!-- Contact Form with Security Controls -->
<form hx-post="/api/contact"
hx-trigger="submit"
hx-target="#contact-result"
_="on htmx:afterRequest
if event.detail.successful
reset() me
end">
<!-- CSRF Token (hidden) -->
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<!-- Timestamp (for timing validation) -->
<input type="hidden" name="timestamp" value="" id="form-timestamp">
<!-- Honeypot field (hidden from real users, bots will fill it) -->
<input type="text"
name="website"
id="website"
style="position:absolute;left:-9999px;"
tabindex="-1"
autocomplete="off">
<!-- Real fields -->
<div>
<label for="name">Name *</label>
<input type="text"
name="name"
id="name"
required
maxlength="100"
pattern="[a-zA-Z\s'-]+"
title="Name can only contain letters, spaces, hyphens, and apostrophes">
</div>
<div>
<label for="email">Email *</label>
<input type="email"
name="email"
id="email"
required
maxlength="254"
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
title="Please enter a valid email address">
</div>
<div>
<label for="company">Company</label>
<input type="text"
name="company"
id="company"
maxlength="100">
</div>
<div>
<label for="subject">Subject *</label>
<input type="text"
name="subject"
id="subject"
required
maxlength="200"
pattern="[a-zA-Z0-9\s.,!?\-]+"
title="Subject can only contain letters, numbers, spaces, and basic punctuation">
</div>
<div>
<label for="message">Message *</label>
<textarea name="message"
id="message"
required
maxlength="5000"
rows="6"></textarea>
</div>
<button type="submit">Send Message</button>
</form>
<div id="contact-result"></div>
<script>
// Set form load timestamp for timing validation
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('form-timestamp').value = Math.floor(Date.now() / 1000);
});
</script>
```
---
## Security Logging Implementation
### Structured Security Logger
```go
package middleware
import (
"encoding/json"
"log"
"net/http"
"time"
)
type SecurityEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
Severity string `json:"severity"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Method string `json:"method"`
Path string `json:"path"`
Details string `json:"details"`
}
// LogSecurityEvent logs security-related events in structured format
func LogSecurityEvent(eventType string, r *http.Request, details string) {
severity := getSeverity(eventType)
event := SecurityEvent{
Timestamp: time.Now(),
EventType: eventType,
Severity: severity,
IP: getClientIP(r),
UserAgent: r.Header.Get("User-Agent"),
Method: r.Method,
Path: r.URL.Path,
Details: details,
}
// JSON format for easy parsing by SIEM systems
eventJSON, _ := json.Marshal(event)
log.Printf("[SECURITY] %s", eventJSON)
// Also log to separate security log file in production
if os.Getenv("GO_ENV") == "production" {
logToSecurityFile(eventJSON)
}
}
func getSeverity(eventType string) string {
switch eventType {
case "BLOCKED", "CSRF_VIOLATION", "ORIGIN_VIOLATION":
return "HIGH"
case "RATE_LIMIT_EXCEEDED", "VALIDATION_FAILED":
return "MEDIUM"
case "CONTACT_FORM_SENT", "PDF_GENERATED":
return "INFO"
default:
return "LOW"
}
}
func getClientIP(r *http.Request) string {
// Check X-Forwarded-For header (proxy/load balancer)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.Split(xff, ",")[0]
}
// Check X-Real-IP header
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
// Fallback to RemoteAddr
return strings.Split(r.RemoteAddr, ":")[0]
}
func logToSecurityFile(eventJSON []byte) {
// Append to /var/log/cv-security.log in production
f, err := os.OpenFile("/var/log/cv-security.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Printf("Failed to open security log: %v", err)
return
}
defer f.Close()
f.Write(eventJSON)
f.WriteString("\n")
}
```
### Enhanced Rate Limiter with Logging
```go
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getClientIP(r)
if !rl.allow(ip) {
// Log rate limit violation
LogSecurityEvent("RATE_LIMIT_EXCEEDED", r,
fmt.Sprintf("IP: %s, Limit: %d/%v", ip, rl.limit, rl.window))
w.Header().Set("Retry-After", "60")
http.Error(w, "Rate limit exceeded. Please try again later.",
http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
```
### Enhanced Origin Checker with Logging
```go
func OriginChecker(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")
// ... existing validation logic ...
if origin != "" && !isAllowedOrigin(origin, allowedOrigins) {
LogSecurityEvent("ORIGIN_VIOLATION", r,
fmt.Sprintf("Blocked origin: %s", origin))
http.Error(w, "Forbidden: External access not allowed",
http.StatusForbidden)
return
}
// ... rest of validation ...
})
}
```
---
## Security Headers Enhancement
### Additional Headers to Add
```go
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Existing headers (keep all current headers)
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "...")
w.Header().Set("Content-Security-Policy", "...")
// NEW: Additional security headers
// Prevent Adobe Flash/PDF from loading external content
w.Header().Set("X-Permitted-Cross-Domain-Policies", "none")
// Cross-Origin isolation
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
// Prevent DNS prefetching of external domains
w.Header().Set("X-DNS-Prefetch-Control", "off")
// HSTS (production only) - already implemented ✅
if os.Getenv("GO_ENV") == "production" {
w.Header().Set("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload")
}
next.ServeHTTP(w, r)
})
}
```
---
## Linux Server Hardening Checklist
### System Security
```bash
# 1. Firewall (UFW)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
# 2. Fail2ban (Brute-force protection)
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Create jail for CV app (detect repeated 403/429 responses)
cat > /etc/fail2ban/jail.d/cv-app.conf << EOF
[cv-app]
enabled = true
port = http,https
filter = cv-app
logpath = /var/log/cv-security.log
maxretry = 10
findtime = 3600
bantime = 86400
EOF
# 3. SSH Hardening
sudo nano /etc/ssh/sshd_config
# Change:
# PermitRootLogin no
# PasswordAuthentication no
# PubkeyAuthentication yes
# Port 2222 (non-standard port)
sudo systemctl restart sshd
# 4. Automatic Security Updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# 5. AppArmor (Application sandboxing)
sudo apt install apparmor apparmor-utils
sudo systemctl enable apparmor
sudo systemctl start apparmor
# 6. Kernel Hardening
sudo nano /etc/sysctl.conf
# Add:
# net.ipv4.conf.all.rp_filter = 1
# net.ipv4.conf.default.rp_filter = 1
# net.ipv4.icmp_echo_ignore_all = 1
# net.ipv4.conf.all.accept_redirects = 0
# net.ipv4.conf.all.send_redirects = 0
# net.ipv4.conf.all.accept_source_route = 0
# net.ipv4.tcp_syncookies = 1
# kernel.dmesg_restrict = 1
sudo sysctl -p
# 7. File Permissions
sudo chmod 600 /etc/ssh/sshd_config
sudo chmod 700 ~/.ssh
sudo chmod 600 ~/.ssh/authorized_keys
sudo chmod 755 /opt/cv-app
sudo chmod 500 /opt/cv-app/cv-server # Read + execute only
```
### Nginx Security Configuration
```nginx
# /etc/nginx/sites-available/cv-app
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=contact:10m rate=5r/h;
limit_req_zone $binary_remote_addr zone=pdf:10m rate=3r/m;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 443 ssl http2;
server_name juan.andres.morenorub.io;
# SSL Configuration (A+ rating)
ssl_certificate /etc/letsencrypt/live/juan.andres.morenorub.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/juan.andres.morenorub.io/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/juan.andres.morenorub.io/chain.pem;
# OCSP Stapling
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security Headers (belt-and-suspenders with Go app)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# CSP (delegated to Go app, but backup here)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://matomo.morenorub.io; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.iconify.design https://matomo.morenorub.io; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;
# Hide Nginx version
server_tokens off;
# Connection limits
limit_conn addr 10;
# General rate limiting
limit_req zone=general burst=20 nodelay;
# Proxy to Go application
location / {
proxy_pass http://127.0.0.1:1999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Contact form endpoint - stricter rate limit
location /api/contact {
limit_req zone=contact burst=1 nodelay;
proxy_pass http://127.0.0.1:1999;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# PDF endpoint - rate limit
location /export/pdf {
limit_req zone=pdf burst=1 nodelay;
proxy_pass http://127.0.0.1:1999;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Extended timeout for PDF generation
proxy_read_timeout 120s;
}
# Static files with caching
location /static/ {
alias /opt/cv-app/static/;
expires 1d;
add_header Cache-Control "public, immutable";
# Security headers for static files
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
# Block access to sensitive files
location ~ /\. {
deny all;
}
location ~ \.(env|git|md|log)$ {
deny all;
}
# Access and error logs
access_log /var/log/nginx/cv-app-access.log combined;
error_log /var/log/nginx/cv-app-error.log warn;
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name juan.andres.morenorub.io;
return 301 https://$server_name$request_uri;
}
```
---
## Dependency Security
### Go Modules
```bash
# Check for updates
go list -m -u all
# Audit dependencies
go mod verify
# Vulnerability scanning (install govulncheck)
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Update dependencies
go get -u ./...
go mod tidy
```
### Frontend Dependencies
```bash
# HTMX and Hyperscript are self-hosted (update by downloading new versions)
# HTMX: static/htmx/htmx.min.js (currently 2.0.10)
# Hyperscript: static/hyperscript/_hyperscript.min.js (currently 0.9.91)
# Iconify (CDN): https://cdn.jsdelivr.net/npm/iconify-icon@latest
# Generate SRI hashes
https://www.srihash.org/
```
---
## Compliance Considerations
### GDPR (General Data Protection Regulation)
- ✅ No personal data collection (current state)
- ⚠️ Contact form will collect: name, email, company, message
- **Requirement:** Privacy policy page
- **Requirement:** Cookie consent banner (if tracking)
- **Requirement:** Data retention policy
- **Requirement:** Right to deletion (email-based request)
### WCAG 2.1 AA (Accessibility)
- ✅ Semantic HTML structure
- ✅ Keyboard navigation support
- ⚠️ Contact form should have proper labels and ARIA attributes
### Browser Security
- ✅ CSP prevents XSS
- ✅ HTTPS enforced (HSTS)
- ✅ Cookies with Secure, HttpOnly, SameSite flags
---
## Penetration Testing Guide
### Manual Testing Checklist
#### 1. XSS Testing
```bash
# Test name field
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"<script>alert(1)</script>","email":"test@test.com"}'
# Expected: Rejected or escaped
# Test message field
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"message":"<img src=x onerror=alert(1)>"}'
# Expected: HTML tags stripped
```
#### 2. Email Header Injection
```bash
# Attempt to inject BCC header
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Test\r\nBcc: attacker@evil.com","email":"test@test.com"}'
# Expected: Rejected (newlines stripped)
# Attempt to inject additional headers
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"subject":"Test\nContent-Type: text/html","email":"test@test.com"}'
# Expected: Rejected
```
#### 3. Rate Limiting
```bash
# Test contact form rate limit (should allow 5/hour)
for i in {1..6}; do
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","message":"Test"}' &
done
wait
# Expected: 6th request returns 429 Too Many Requests
# Test PDF rate limit (should allow 3/minute)
for i in {1..4}; do
curl "https://juan.andres.morenorub.io/export/pdf?lang=en" -o /dev/null &
done
wait
# Expected: 4th request returns 429
```
#### 4. Origin Validation
```bash
# Test contact form from curl (should be blocked)
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com"}'
# Expected: 403 Forbidden
# Test with fake Origin header
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Origin: https://evil.com" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com"}'
# Expected: 403 Forbidden
# Test PDF endpoint from curl
curl "https://juan.andres.morenorub.io/export/pdf?lang=en"
# Expected: 403 Forbidden (in production)
```
#### 5. CSRF Testing
```bash
# Attempt CSRF attack without token
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-H "Cookie: session=valid_session" \
-d '{"name":"Test","email":"test@test.com"}'
# Expected: 403 Forbidden (missing CSRF token)
# Attempt with invalid token
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-H "Cookie: csrf_token=valid_token" \
-d '{"csrf_token":"invalid_token","name":"Test","email":"test@test.com"}'
# Expected: 403 Forbidden (token mismatch)
```
#### 6. SQL Injection (Should be N/A)
```bash
# No database, but test input sanitization
curl -X POST https://juan.andres.morenorub.io/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Robert\"; DROP TABLE users; --","email":"test@test.com"}'
# Expected: Rejected or escaped (no SQL execution anyway)
```
### Automated Scanning Tools
```bash
# OWASP ZAP (Zed Attack Proxy)
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://juan.andres.morenorub.io
# Nikto web scanner
nikto -h https://juan.andres.morenorub.io
# SSL/TLS testing
testssl.sh --severity HIGH https://juan.andres.morenorub.io
# Security headers check
curl -I https://juan.andres.morenorub.io | grep -E "(X-|Content-Security|Strict-Transport)"
# CSP validator
https://csp-evaluator.withgoogle.com/
```
---
## Security Metrics & Monitoring
### Key Metrics to Track
1. **Rate limit violations** - Track IPs hitting rate limits
2. **Origin validation failures** - Detect hotlinking attempts
3. **CSRF validation failures** - Potential attack indicators
4. **Failed form submissions** - Bot detection effectiveness
5. **PDF generation errors** - Potential DoS attempts
6. **Suspicious user agents** - Bot/scraper activity
### Monitoring Setup
```bash
# Security log monitoring (production)
tail -f /var/log/cv-security.log | jq 'select(.severity == "HIGH")'
# Real-time rate limit violations
grep "RATE_LIMIT_EXCEEDED" /var/log/cv-security.log | tail -n 20
# Geographic distribution of blocked requests
grep "BLOCKED" /var/log/cv-security.log | jq -r '.ip' | sort | uniq -c | sort -rn
# Top blocked user agents
grep "BLOCKED" /var/log/cv-security.log | jq -r '.user_agent' | sort | uniq -c | sort -rn
```
### Alerting Rules (for Prometheus/Grafana)
```yaml
# Alert on high rate limit violations
- alert: HighRateLimitViolations
expr: rate(cv_rate_limit_violations_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High rate limit violations detected"
# Alert on CSRF attacks
- alert: CSRFAttackDetected
expr: increase(cv_csrf_violations_total[1h]) > 5
for: 1m
labels:
severity: critical
annotations:
summary: "CSRF attack detected"
# Alert on origin validation failures
- alert: HotlinkingAttempt
expr: rate(cv_origin_violations_total[5m]) > 5
for: 5m
labels:
severity: medium
annotations:
summary: "Potential hotlinking attack detected"
```
---
## Incident Response Playbook
### 1. Rate Limit Attack (DoS)
**Indicators:**
- Spike in 429 responses
- Single IP hitting rate limits repeatedly
**Response:**
1. Identify attacking IP: `grep "RATE_LIMIT_EXCEEDED" /var/log/cv-security.log | tail -n 100`
2. Ban IP with fail2ban: `sudo fail2ban-client set cv-app banip <IP>`
3. Review logs for patterns
4. Consider lowering rate limits temporarily
### 2. CSRF Attack
**Indicators:**
- Multiple CSRF validation failures
- Attempts from different IPs with same pattern
**Response:**
1. Rotate CSRF secret
2. Review user sessions
3. Check for XSS vulnerability that could steal tokens
4. Increase logging verbosity
### 3. Email Header Injection Attempt
**Indicators:**
- Contact form submissions with newlines in headers
- Failed validation for email fields
**Response:**
1. Verify sanitization is working
2. Check email logs for suspicious sends
3. Review all contact form submissions from that IP
4. Ban IP if repeated attempts
### 4. Brute Force Attack
**Indicators:**
- Repeated failed requests from same IP
- Multiple POST requests in short time
**Response:**
1. Verify rate limiting is active
2. Ban IP with fail2ban
3. Review user agents (might be bot network)
4. Consider CAPTCHA if persistent
---
## Security Testing Schedule
### Daily
- ✅ Review security logs for anomalies
- ✅ Check fail2ban banned IPs
### Weekly
- ✅ Run `govulncheck` for dependency vulnerabilities
- ✅ Review rate limit violations
- ✅ Check SSL certificate expiry
### Monthly
- ✅ Update Go dependencies
- ✅ Run full OWASP ZAP scan
- ✅ Review and rotate logs
- ✅ Test backup/restore procedures
### Quarterly
- ✅ Full penetration test (manual + automated)
- ✅ Security audit review
- ✅ Update security policies
- ✅ Review and update rate limits based on traffic
---
## Summary of Required Actions
### CRITICAL (Implement before contact form goes live)
1.**Origin Validation for Contact Form** - Browser-only access
2.**CSRF Token System** - Generate, validate, rotate
3.**Input Validation** - Email, name, subject, message sanitization
4.**Email Header Injection Prevention** - Strip newlines, validate headers
5.**Rate Limiting** - 5 requests/hour for contact endpoint
6.**Bot Protection** - Honeypot + timing validation
7.**Security Logging** - Track all security events
### HIGH PRIORITY
1. ⚠️ Add SRI hashes for Hyperscript and Iconify
2. ⚠️ Implement structured security logging
3. ⚠️ Set up fail2ban for repeated attacks
4. ⚠️ Configure Nginx with security headers and rate limits
### MEDIUM PRIORITY
1. ⚠️ Add additional security headers (X-Permitted-Cross-Domain-Policies, etc.)
2. ⚠️ Implement automated dependency scanning
3. ⚠️ Set up security monitoring dashboard
4. ⚠️ Create privacy policy for GDPR compliance
### LOW PRIORITY (Nice to have)
1. ️ CSP nonce-based script loading (instead of 'unsafe-inline')
2. ️ Security bug bounty program
3. ️ Penetration testing by third party
4. ️ SOC 2 Type II compliance (if needed for clients)
---
## Risk Matrix
| Risk | Likelihood | Impact | Severity | Mitigation |
|------|-----------|---------|----------|------------|
| XSS Attack | Low | High | Medium | ✅ html/template auto-escaping |
| CSRF Attack | Medium | Low | Low | ⚠️ Implement tokens |
| Rate Limit DoS | Medium | Medium | Medium | ✅ Rate limiting active |
| Email Header Injection | Low | High | Medium | ⚠️ Implement sanitization |
| SQL Injection | N/A | N/A | N/A | ✅ No database |
| Command Injection | Very Low | Critical | Low | ✅ go-git library used |
| DDoS Attack | Medium | Medium | Medium | ⚠️ Cloudflare/rate limiting |
| Brute Force | Low | Low | Low | ✅ Rate limiting |
| Path Traversal | Very Low | High | Low | ✅ Validation implemented |
| Dependency Vuln | Medium | Medium | Medium | ⚠️ Regular updates needed |
---
## Conclusion
The CV application demonstrates **strong security fundamentals** with proper XSS prevention, command injection mitigation, and comprehensive security headers. The main areas requiring attention are:
1. **CSRF protection** for POST endpoints (low risk, but should implement)
2. **Security logging** for attack detection and incident response
3. **Contact form security** (when implemented) with strict validation and origin checks
**Overall Security Rating: B+ (GOOD)**
With the recommended improvements, the application can achieve an **A+ security rating**.
---
**Next Steps:**
1. Review this report with the development team
2. Implement contact form with security controls (see design above)
3. Add CSRF protection to existing POST endpoints
4. Set up security logging and monitoring
5. Test all security controls with penetration testing tools
6. Deploy to production with Nginx security configuration
7. Monitor security logs and iterate on defenses
**Security is a continuous process, not a destination. Regular audits, updates, and monitoring are essential.**