diff --git a/backend/app/administration.go b/backend/app/administration.go index 6f92353..b2b84fb 100644 --- a/backend/app/administration.go +++ b/backend/app/administration.go @@ -220,12 +220,13 @@ const ( // administrationServer is the administrationServer app type administrationServer struct { - Server *http.Server - router *gin.Engine - logger *zap.SugaredLogger - production bool - embedBackendFS *embed.FS - certMagicConfig *certmagic.Config + Server *http.Server + router *gin.Engine + logger *zap.SugaredLogger + production bool + embedBackendFS *embed.FS + certMagicConfig *certmagic.Config + softSessionHandler gin.HandlerFunc } // NewAdministrationServer creates a new administration app @@ -240,10 +241,11 @@ func NewAdministrationServer( router = setupRoutes(router, controllers, middlewares) return &administrationServer{ - router: router, - logger: logger, - production: production, - certMagicConfig: certMagicConfig, + router: router, + logger: logger, + production: production, + certMagicConfig: certMagicConfig, + softSessionHandler: middlewares.SoftSessionHandler, } } @@ -713,6 +715,15 @@ func (a *administrationServer) loadEmbeddedFileSystem( embedFS := frontend.GetEmbededFS() // make embedded .html work frontend.LoadHTMLFromEmbedFS(a.router, *embedFS, "build/*.html") + // serve favicons only to authenticated users — unauthenticated requests get 404 + // so the file is not indexable by scanners probing common paths + for _, faviconPath := range []string{"/favicon.ico", "/favicon.png"} { + fp := faviconPath + a.router.GET(fp, a.softSessionHandler, func(c *gin.Context) { + name := fp[1:] // strip leading / + c.FileFromFS("build/"+name, http.FS(*embedFS)) + }) + } rootDir, err := embedFS.ReadDir("build") if err != nil { return errs.Wrap(err) @@ -728,6 +739,10 @@ func (a *administrationServer) loadEmbeddedFileSystem( }) continue } + // skip favicons — registered separately with session gating + if path == "favicon.png" || path == "favicon.ico" { + continue + } // any file in the root folder gets server as a file a.router.GET("/"+path, func(c *gin.Context) { c.FileFromFS("build/"+path, http.FS(*embedFS)) diff --git a/backend/app/middleware.go b/backend/app/middleware.go index c9b7a0f..6b83886 100644 --- a/backend/app/middleware.go +++ b/backend/app/middleware.go @@ -11,9 +11,10 @@ import ( // Middlwares is a collection of middlewares type Middlewares struct { - IPLimiter gin.HandlerFunc - LoginRateLimiter gin.HandlerFunc - SessionHandler gin.HandlerFunc + IPLimiter gin.HandlerFunc + LoginRateLimiter gin.HandlerFunc + SessionHandler gin.HandlerFunc + SoftSessionHandler gin.HandlerFunc } // NewMiddlewares creates a collection of middlewares @@ -36,11 +37,17 @@ func NewMiddlewares( utils.JSONResponseHandler, logger, ) + softSessionHandler := middleware.NewSoftSessionHandler( + services.Session, + services.User, + logger, + ) return &Middlewares{ - IPLimiter: ipLimiter, - LoginRateLimiter: loginThrottle, - SessionHandler: sessionHandler, + IPLimiter: ipLimiter, + LoginRateLimiter: loginThrottle, + SessionHandler: sessionHandler, + SoftSessionHandler: softSessionHandler, } } diff --git a/backend/middleware/session.go b/backend/middleware/session.go index 62062aa..06dc3d9 100644 --- a/backend/middleware/session.go +++ b/backend/middleware/session.go @@ -3,6 +3,7 @@ package middleware import ( "crypto/sha256" "crypto/subtle" + "net/http" "time" "github.com/gin-gonic/gin" @@ -15,6 +16,32 @@ import ( "go.uber.org/zap" ) +// NewSoftSessionHandler is like NewSessionHandler but returns 404 on auth failure +// instead of 401, so unauthenticated requests are indistinguishable from missing resources. +func NewSoftSessionHandler( + sessionService *service.Session, + userService *service.User, + logger *zap.SugaredLogger, +) gin.HandlerFunc { + return func(c *gin.Context) { + isValidAPISession := handleAPISession(c, userService, logger) + if isValidAPISession { + return + } + s, err := sessionService.GetAndExtendSession(c) + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + if s.User == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + controller.SetSessionInGinContext(c, s) + c.Next() + } +} + // NewSessionHandler creates a middleware that authenticates the user // by checking it has a session, and if it does, it extends the session and puts // the user and the session in the gin context.