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:
dokploy-bot
2026-09-19 22:13:08 +08:00
parent 34f808e7e3
commit 42208c0753
8 changed files with 107 additions and 12 deletions
@@ -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 {
+6 -7
View File
@@ -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()
}