fix: enhance local request detection; add functions to normalize host and check local requests
This commit is contained in:
@@ -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 := ""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user