fix: security hardening — bcrypt cost, secure cookies, security headers, avatar auth Bump bcrypt cost from 10 to 12 for adequate brute-force resistance in 2026. Add behind_proxy config option that enables Secure flag on session cookies for deployments behind a TLS-terminating reverse proxy (Caddy, nginx). Add X-Content-Type-Options, X-Frame-Options, and Referrer-Policy security headers to every response. Fix avatar endpoint auth mismatch — avatars are now served publicly since their URLs are exposed in gallery listings anyway. Co-authored-by: Crush
Maxwell Jensen maxwelljensen@posteo.net
Wed, 13 May 2026 19:56:35 +0200
8 files changed,
58 insertions(+),
9 deletions(-)
M
README.md
→
README.md
@@ -22,11 +22,14 @@ The core loop is dead simple. Register an account (or have an admin make one
for you), upload files through the web form with whatever tags make sense, and they appear in the gallery. Click any thumbnail to open the full image in an overlay viewer: arrow keys to browse, `Esc` to close, click outside to dismiss. +The URL updates to a shareable link (`/browse?image=<id>`) that you can send to +anyone — even people without an account, because browsing is public by default. Tags are how you find things later. Type a few words when uploading and the autocomplete will suggest tags other people have used. Filter the gallery by -clicking tags or adding `?tag=funny` to the URL. Each image gets a permanent -page you can share, a download link, and an auto-generated thumbnail. +clicking tags or adding `?tag=funny` to the URL. If you're the uploader, you +can edit tags after the fact: a text field with the same autocomplete and ADD +and REMOVE buttons appears below the tag display in the viewer. ## Where data lives on disk@@ -67,6 +70,26 @@ max_size_mb = 50
[auth] allow_registration = true +``` + +If you're putting weimar behind a TLS-terminating reverse proxy like Caddy or +nginx (which you should if facing the Internet), add this: + +```toml +[server] +behind_proxy = true +``` + +This enables the `Secure` flag on session cookies so your browser doesn't refuse +to send them over the HTTPS connection to the proxy. The security headers +(`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, +`Referrer-Policy: same-origin`) are always set regardless. + +You can also require authentication to browse the gallery: + +```toml +[auth] +require_auth_for_browse = true ``` If you don't want to bother with a config file at all, you can set every option
M
cmd/weimar/root.go
→
cmd/weimar/root.go
@@ -75,6 +75,7 @@
func setDefaults() { viper.SetDefault("server.host", "0.0.0.0") viper.SetDefault("server.port", 8080) + viper.SetDefault("server.behind_proxy", false) viper.SetDefault("database.path", "./data/weimar.db") viper.SetDefault("storage.path", "./data/images") viper.SetDefault("upload.max_size_mb", 50)
M
cmd/weimar/serve.go
→
cmd/weimar/serve.go
@@ -78,6 +78,8 @@ func init() {
rootCmd.AddCommand(serveCmd) serveCmd.Flags().StringVar(&adminEmail, "admin-email", "", "admin email for first-run setup") serveCmd.Flags().StringVar(&adminPassword, "admin-password", "", "admin password for first-run setup") + serveCmd.Flags().Bool("behind-proxy", false, "set Secure flag on cookies and trust X-Forwarded headers") + viper.BindPFlag("server.behind_proxy", serveCmd.Flags().Lookup("behind-proxy")) viper.BindPFlag("server.host", serveCmd.Flags().Lookup("host")) viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port")) }
M
internal/api/auth.go
→
internal/api/auth.go
@@ -43,6 +43,7 @@ Name: auth.SessionCookieName,
Value: token, Path: "/", HttpOnly: true, + Secure: a.Cfg.Server.SecureCookie(), SameSite: http.SameSiteLaxMode, })@@ -81,6 +82,7 @@ Name: auth.SessionCookieName,
Value: token, Path: "/", HttpOnly: true, + Secure: a.Cfg.Server.SecureCookie(), SameSite: http.SameSiteLaxMode, }) }
M
internal/auth/register.go
→
internal/auth/register.go
@@ -20,7 +20,7 @@
const ( MinPasswordLength = 8 MinUsernameLength = 3 - bcryptCost = 10 + bcryptCost = 12 ) // Register creates a new user account. If allowRegistration is false it
M
internal/config/config.go
→
internal/config/config.go
@@ -11,8 +11,13 @@ Auth AuthConfig
} type ServerConfig struct { - Host string - Port int + Host string + Port int + BehindProxy bool `mapstructure:"behind_proxy"` +} + +func (s ServerConfig) SecureCookie() bool { + return s.BehindProxy } type DatabaseConfig struct {@@ -36,8 +41,9 @@
func FromViper() Config { return Config{ Server: ServerConfig{ - Host: viper.GetString("server.host"), - Port: viper.GetInt("server.port"), + Host: viper.GetString("server.host"), + Port: viper.GetInt("server.port"), + BehindProxy: viper.GetBool("server.behind_proxy"), }, Database: DatabaseConfig{ Path: viper.GetString("database.path"),
M
internal/server/server.go
→
internal/server/server.go
@@ -54,7 +54,7 @@
// Users (authenticated). mux.HandleFunc("GET /api/users/me", s.requireAuth(h.HandleGetMe)) mux.HandleFunc("POST /api/users/me/avatar", s.requireAuth(h.HandleUploadAvatar)) - mux.HandleFunc("GET /api/users/{id}/avatar", s.requireAuth(h.HandleServeAvatar)) + mux.HandleFunc("GET /api/users/{id}/avatar", h.HandleServeAvatar) // SPA — serve index.html for all non-API routes (client-side routing). mux.Handle("/", s.spaHandler(webFS))@@ -70,7 +70,11 @@ }
func (s *Server) Start(ctx context.Context) error { addr := s.cfg.Server.Host + ":" + itoa(s.cfg.Server.Port) - srv := &http.Server{Addr: addr, Handler: s.mux} + + // Wrap the mux with a handler that sets security headers. + handler := securityHeaders(s.mux) + + srv := &http.Server{Addr: addr, Handler: handler} go func() { <-ctx.Done()@@ -124,3 +128,13 @@ func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) } + +// securityHeaders wraps a handler with security-related HTTP headers. +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Referrer-Policy", "same-origin") + next.ServeHTTP(w, r) + }) +}