security: fix rate-limit bypass, payment amount check, stats leak, owner-dir collision, video panic guard
- clientIP: stop trusting client-supplied X-Forwarded-For; use gin ClientIP with trusted proxies limited to private docker networks - payment: verify notify money matches order amount; remove hardcoded gateway default, fail loudly when api_base is unset - stats: public endpoint now serves aggregates only; recent files (owner names) moved to admin-only /stats/recent - auth: reject signups whose username/email-local collides with another user's media directory namespace - v1: recover panics in background video jobs, refund + mark failed
This commit is contained in:
@@ -187,6 +187,17 @@ func (h *AdminReadHandler) Stats(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// StatsRecent serves the recent-files list (owner names included) — only
|
||||
// mounted on the admin-session route group.
|
||||
func (h *AdminReadHandler) StatsRecent(c *gin.Context) {
|
||||
data, err := h.admin.StatsRecent(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load recent files"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (h *AdminReadHandler) Dashboard(c *gin.Context) {
|
||||
data, err := h.admin.Dashboard(c.Request.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -307,14 +307,13 @@ func writeLoginLocked(c *gin.Context, err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// clientIP resolves the caller for rate limiting. It must NOT read
|
||||
// client-supplied X-Forwarded-For / X-Real-Ip directly: the first XFF value
|
||||
// is attacker-controlled and rotating it resets every throttle bucket.
|
||||
// gin's ClientIP walks the header chain right-to-left and only honors
|
||||
// forwarding headers from the trusted proxies configured in the router
|
||||
// (the in-stack traefik/nginx on private docker networks).
|
||||
func clientIP(c *gin.Context) string {
|
||||
if fwd := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); fwd != "" {
|
||||
parts := strings.Split(fwd, ",")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
if real := strings.TrimSpace(c.GetHeader("X-Real-Ip")); real != "" {
|
||||
return real
|
||||
}
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
||||
}
|
||||
|
||||
engine := gin.New()
|
||||
// Only the in-stack reverse proxies (traefik → frontend nginx, all on
|
||||
// private docker networks) may supply forwarding headers; ClientIP then
|
||||
// resolves the real client instead of a spoofable X-Forwarded-For.
|
||||
_ = engine.SetTrustedProxies([]string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
})
|
||||
engine.Use(gin.Recovery())
|
||||
engine.Use(middleware.RequestID())
|
||||
engine.Use(cors.New(cors.Config{
|
||||
@@ -64,6 +74,8 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
||||
publicAdmin.GET("/site", handlers.Site.Public)
|
||||
publicAdmin.GET("/showcase", handlers.Showcase.List)
|
||||
publicAdmin.GET("/managed-models", handlers.AdminRead.Models)
|
||||
// Aggregates only (counts/sizes/latency) — the recent-files list with
|
||||
// owner names is admin-only, see /stats/recent below.
|
||||
publicAdmin.GET("/stats", handlers.AdminRead.Stats)
|
||||
publicAdmin.GET("/video-presets", handlers.UserGen.VideoPresets)
|
||||
publicAdmin.GET("/catalog", handlers.UserGen.Catalog)
|
||||
@@ -106,6 +118,8 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
||||
authed.Use(middleware.RequireAdminSession(auth, cfg))
|
||||
{
|
||||
authed.GET("/dashboard", handlers.AdminRead.Dashboard)
|
||||
// Recent generated files include owner-directory names — admin only.
|
||||
authed.GET("/stats/recent", handlers.AdminRead.StatsRecent)
|
||||
authed.GET("/users", handlers.AdminRead.Users)
|
||||
authed.GET("/invites", handlers.AdminRead.Invites)
|
||||
authed.POST("/users", handlers.AdminWrite.CreateUser)
|
||||
|
||||
Reference in New Issue
Block a user