Compare commits

...

4 Commits

Author SHA1 Message Date
RedCrazyGhost
6df21e7a58
Merge 5ea1f87e8838da6ff3d02435f979778342fe1af4 into 9914178584e42458ff7d23891463a880f58c9d86 2026-01-12 12:41:49 -08:00
Nurysso
9914178584
fix(context): ClientIP handling for multiple X-Forwarded-For header values (#4472)
* Fix ClientIP calculation by concatenating all RemoteIPHeaders values

* test: used http.MethodGet instead constants and fix lints

* lint error fixed

* Refactor ClientIP X-Forwarded-For tests

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-02 10:15:27 +08:00
Paulo Henrique
915e4c90d2
refactor(context): replace hardcoded localhost IPs with constants (#4481) 2025-12-27 19:25:17 +08:00
Ghost
5ea1f87e88 choer: change redirect note conetxt 2024-09-04 00:18:10 +08:00
5 changed files with 52 additions and 4 deletions

View File

@ -989,7 +989,8 @@ func (c *Context) ClientIP() string {
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
ip, valid := c.engine.validateHeader(headerValue)
if valid {
return ip
}
@ -1219,7 +1220,14 @@ func (c *Context) String(code int, format string, values ...any) {
}
// Redirect returns an HTTP redirect to the specific location.
// When the 'location' parameter is empty, it poses a potential security risk.
// Avoid bringing potential security risks into the production environment.
func (c *Context) Redirect(code int, location string) {
if location == "" {
debugPrint(`[WARNING] When the 'location' parameter is empty, it poses a potential security risk. Please input a secure redirection URL to ensure safe operation.`)
}
c.Render(-1, render.Redirect{
Code: code,
Location: location,

View File

@ -1143,6 +1143,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
// Multiple X-Forwarded-For headers
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 5.6.7.8 (last non-trusted IP)
assert.Equal(t, "5.6.7.8", c.ClientIP())
}
func TestContextClientIPWithSingleHeader(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 1.2.3.4
assert.Equal(t, "1.2.3.4", c.ClientIP())
}
// Tests that the response is serialized as Secure JSON
// and Content-Type is set to application/json
func TestContextRenderSecureJSON(t *testing.T) {
@ -1548,6 +1579,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.Panics(t, func() { c.Redirect(309, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusTemporaryRedirect, "")})
}
func TestContextNegotiationWithJSON(t *testing.T) {
@ -1910,7 +1942,7 @@ func TestContextClientIP(t *testing.T) {
resetContextForClientIPTests(c)
// IPv6 support
c.Request.RemoteAddr = "[::1]:12345"
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
assert.Equal(t, "20.20.20.20", c.ClientIP())
resetContextForClientIPTests(c)
@ -3212,7 +3244,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
}()
addr := strings.Split(l.Addr().String(), ":")
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
if err != nil {
t.Error(fmt.Errorf("request error: %w", err))
return

View File

@ -1555,6 +1555,8 @@ Gin allows only one html.Template by default. Check [a multitemplate render](htt
Issuing a HTTP redirect is easy. Both internal and external locations are supported.
Note: When the location is empty, there is a security risk. Please do not bring it to production
```go
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")

View File

@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
}
func TestH2c(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
ln, err := net.Listen("tcp", localhostIP+":0")
if err != nil {
t.Error(err)
}

View File

@ -19,6 +19,12 @@ import (
// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey"
// localhostIP indicates the default localhost IP address.
const localhostIP = "127.0.0.1"
// localhostIPv6 indicates the default localhost IPv6 address.
const localhostIPv6 = "::1"
// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val any) HandlerFunc {
value := reflect.ValueOf(val)