2026-05-07 23:36:11 +02:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-13 12:52:52 +02:00
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
2026-05-07 23:36:11 +02:00
|
|
|
"net/http"
|
|
|
|
|
|
2026-05-10 02:13:42 +02:00
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/session"
|
2026-05-07 23:36:11 +02:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SecurityHeadersMiddleware adds browser hardening headers to panel responses.
|
|
|
|
|
func SecurityHeadersMiddleware(directHTTPS bool) gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
2026-05-13 12:52:52 +02:00
|
|
|
nonce := newCSPNonce()
|
|
|
|
|
c.Set("csp_nonce", nonce)
|
2026-05-07 23:36:11 +02:00
|
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
|
|
|
c.Header("X-Frame-Options", "DENY")
|
|
|
|
|
c.Header("Referrer-Policy", "no-referrer")
|
2026-05-13 12:52:52 +02:00
|
|
|
c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'nonce-"+nonce+"'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'")
|
2026-05-07 23:36:11 +02:00
|
|
|
if directHTTPS {
|
|
|
|
|
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
|
|
|
}
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 12:52:52 +02:00
|
|
|
func newCSPNonce() string {
|
|
|
|
|
var b [16]byte
|
|
|
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return base64.RawStdEncoding.EncodeToString(b[:])
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 23:36:11 +02:00
|
|
|
// CSRFMiddleware rejects unsafe requests that do not include the session CSRF token.
|
2026-05-09 17:38:48 +02:00
|
|
|
// Bearer-token-authenticated callers (api_authed flag set by APIController.checkAPIAuth)
|
|
|
|
|
// short-circuit the CSRF check — they are not browser sessions, so the
|
|
|
|
|
// cross-site request forgery threat model doesn't apply to them.
|
2026-05-07 23:36:11 +02:00
|
|
|
func CSRFMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
2026-05-09 17:38:48 +02:00
|
|
|
if c.GetBool("api_authed") {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-07 23:36:11 +02:00
|
|
|
if isSafeMethod(c.Request.Method) {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !session.ValidateCSRFToken(c) {
|
|
|
|
|
c.AbortWithStatus(http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isSafeMethod(method string) bool {
|
|
|
|
|
switch method {
|
|
|
|
|
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|