Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package middleware
|
||||
|
||||
// Reference: P14 Deliverable #3 — Security Headers Middleware
|
||||
// Applies comprehensive HTTP security headers to all responses.
|
||||
// Chatwoot uses rack-protection and secure_headers.rb; GoChat needs equivalent middleware.
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// --- Security Audit Findings ---
|
||||
//
|
||||
// 1. CRITICAL: No security headers set on any response.
|
||||
// Chatwoot sets X-Frame-Options, CSP, HSTS via rack-protection.
|
||||
// Without these, the app is vulnerable to clickjacking, MIME sniffing,
|
||||
// and XSS injection attacks.
|
||||
//
|
||||
// 2. HIGH: Server header discloses Go version — enables targeted attacks.
|
||||
// Must be removed or replaced with generic value.
|
||||
//
|
||||
// 3. HIGH: No HSTS header — browser won't enforce HTTPS.
|
||||
// Chatwoot forces SSL in production; HSTS ensures browsers remember.
|
||||
//
|
||||
// 4. MEDIUM: No Permissions-Policy — browser APIs (camera, mic, geolocation)
|
||||
// can be silently accessed by compromised scripts.
|
||||
//
|
||||
// 5. MEDIUM: X-XSS-Protection set to "1" can introduce vulnerabilities
|
||||
// in older browsers. Modern best practice is "0" (disable legacy filter,
|
||||
// rely on CSP instead).
|
||||
|
||||
// SecurityHeadersConfig holds configuration for security headers middleware.
|
||||
type SecurityHeadersConfig struct {
|
||||
// HSTSMaxAge specifies the max-age directive for Strict-Transport-Security.
|
||||
// Default: 31536000 (1 year). Set to 0 to disable HSTS.
|
||||
HSTSMaxAge int
|
||||
|
||||
// HSTSIncludeSubDomains adds includeSubDomains to HSTS header.
|
||||
HSTSIncludeSubDomains bool
|
||||
|
||||
// HSTSPreload adds preload to HSTS header for HSTS preload list submission.
|
||||
HSTSPreload bool
|
||||
|
||||
// CSPDirectives specifies Content-Security-Policy directives.
|
||||
// If empty, a secure default policy is applied.
|
||||
CSPDirectives string
|
||||
|
||||
// ReferrerPolicy specifies the Referrer-Policy header value.
|
||||
// Default: "strict-origin-when-cross-origin".
|
||||
ReferrerPolicy string
|
||||
|
||||
// PermissionsPolicy specifies the Permissions-Policy header value.
|
||||
// If empty, a restrictive default is applied.
|
||||
PermissionsPolicy string
|
||||
}
|
||||
|
||||
// DefaultSecurityHeadersConfig returns secure defaults matching
|
||||
// Chatwoot's rack-protection configuration with modern best practices.
|
||||
func DefaultSecurityHeadersConfig() SecurityHeadersConfig {
|
||||
return SecurityHeadersConfig{
|
||||
HSTSMaxAge: 31536000, // 1 year
|
||||
HSTSIncludeSubDomains: true,
|
||||
HSTSPreload: false,
|
||||
CSPDirectives: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
||||
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||
PermissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()",
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityHeaders applies security headers to every response.
|
||||
// Headers applied:
|
||||
// - Content-Security-Policy: prevents XSS, clickjacking, data injection
|
||||
// - X-Frame-Options: DENY — legacy clickjacking protection for older browsers
|
||||
// - X-Content-Type-Options: nosniff — prevents MIME type sniffing
|
||||
// - Strict-Transport-Security: enforces HTTPS in browsers
|
||||
// - X-XSS-Protection: 0 — disables legacy IE XSS filter (modern CSP is better)
|
||||
// - Referrer-Policy: controls referrer information sent to other sites
|
||||
// - Permissions-Policy: restricts browser API access
|
||||
// - Server header is removed to prevent fingerprinting
|
||||
func SecurityHeaders(cfg SecurityHeadersConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Content-Security-Policy — primary XSS defense
|
||||
if cfg.CSPDirectives != "" {
|
||||
c.Header("Content-Security-Policy", cfg.CSPDirectives)
|
||||
}
|
||||
|
||||
// X-Frame-Options: DENY — prevents framing entirely (legacy browsers)
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
|
||||
// X-Content-Type-Options: nosniff — prevents browsers from MIME sniffing
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// Strict-Transport-Security — tells browsers to only use HTTPS
|
||||
if cfg.HSTSMaxAge > 0 {
|
||||
hstsValue := "max-age=" + itoa(cfg.HSTSMaxAge)
|
||||
if cfg.HSTSIncludeSubDomains {
|
||||
hstsValue += "; includeSubDomains"
|
||||
}
|
||||
if cfg.HSTSPreload {
|
||||
hstsValue += "; preload"
|
||||
}
|
||||
c.Header("Strict-Transport-Security", hstsValue)
|
||||
}
|
||||
|
||||
// X-XSS-Protection: 0 — disable legacy XSS filter (modern CSP is preferred)
|
||||
// Setting to "1" can introduce vulnerabilities in IE/Edge legacy mode.
|
||||
c.Header("X-XSS-Protection", "0")
|
||||
|
||||
// Referrer-Policy — control how much referrer info is shared
|
||||
if cfg.ReferrerPolicy != "" {
|
||||
c.Header("Referrer-Policy", cfg.ReferrerPolicy)
|
||||
}
|
||||
|
||||
// Permissions-Policy — restrict access to browser APIs
|
||||
if cfg.PermissionsPolicy != "" {
|
||||
c.Header("Permissions-Policy", cfg.PermissionsPolicy)
|
||||
}
|
||||
|
||||
// Remove Server header to prevent server fingerprinting
|
||||
c.Header("Server", "")
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// itoa converts an int to its decimal string representation.
|
||||
// Avoids importing strconv for a single use.
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user