fix: enhance local request detection; add functions to normalize host and check local requests

This commit is contained in:
GitHub Actions
2026-02-13 18:19:21 +00:00
parent 26970e43d3
commit d8c08c4b5d
2 changed files with 169 additions and 0 deletions

View File

@@ -1,7 +1,9 @@
package handlers
import (
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@@ -47,6 +49,82 @@ func requestScheme(c *gin.Context) string {
return "http"
}
func normalizeHost(rawHost string) string {
host := strings.TrimSpace(rawHost)
if host == "" {
return ""
}
if strings.Contains(host, ":") {
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
host = parsedHost
}
}
return strings.Trim(host, "[]")
}
func originHost(rawURL string) string {
if rawURL == "" {
return ""
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return ""
}
return normalizeHost(parsedURL.Host)
}
func isLocalHost(host string) bool {
if strings.EqualFold(host, "localhost") {
return true
}
if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
return true
}
return false
}
func isLocalRequest(c *gin.Context) bool {
candidates := []string{}
if c.Request != nil {
candidates = append(candidates, normalizeHost(c.Request.Host))
if c.Request.URL != nil {
candidates = append(candidates, normalizeHost(c.Request.URL.Host))
}
candidates = append(candidates,
originHost(c.Request.Header.Get("Origin")),
originHost(c.Request.Header.Get("Referer")),
)
}
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
parts := strings.Split(forwardedHost, ",")
for _, part := range parts {
candidates = append(candidates, normalizeHost(part))
}
}
for _, host := range candidates {
if host == "" {
continue
}
if isLocalHost(host) {
return true
}
}
return false
}
// setSecureCookie sets an auth cookie with security best practices
// - HttpOnly: prevents JavaScript access (XSS protection)
// - Secure: derived from request scheme to allow HTTP/IP logins when needed
@@ -59,6 +137,11 @@ func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
sameSite = http.SameSiteLaxMode
}
if isLocalRequest(c) {
secure = false
sameSite = http.SameSiteLaxMode
}
// Use the host without port for domain
domain := ""

View File

@@ -96,6 +96,92 @@ func TestSetSecureCookie_HTTP_Lax(t *testing.T) {
assert.Equal(t, http.SameSiteLaxMode, c.SameSite)
}
func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
_ = os.Setenv("CHARON_ENV", "production")
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://localhost:8080/login", http.NoBody)
req.Host = "localhost:8080"
req.Header.Set("X-Forwarded-Proto", "https")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
_ = os.Setenv("CHARON_ENV", "production")
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://127.0.0.1:8080/login", http.NoBody)
req.Host = "127.0.0.1:8080"
req.Header.Set("X-Forwarded-Proto", "https")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
_ = os.Setenv("CHARON_ENV", "production")
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://charon.local/login", http.NoBody)
req.Host = "charon.internal:8080"
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "localhost:8080")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
_ = os.Setenv("CHARON_ENV", "production")
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://service.internal/login", http.NoBody)
req.Host = "service.internal:8080"
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("Origin", "http://127.0.0.1:8080")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestAuthHandler_Login_Errors(t *testing.T) {
t.Parallel()
handler, _ := setupAuthHandler(t)