CVE-2023-41338 (published 2023-09-08T19:15Z)

Fiber is an Express inspired web framework built in the go language. Versions of gofiber prior to 2.49.2 did not properly restrict access to localhost. This issue impacts users of our project who rely on the `ctx.IsFromLocal` method to restrict access to localhost requests. If exploited, it could allow unauthorized access to resources intended only for localhost. Setting `X-Forwarded-For: 127.0.0.1` in a request from a foreign host, will result in true for `ctx.IsFromLocal`. Access is limited to the scope of the affected process. This issue has been patched in version `2.49.2` with commit `b8c9ede6`. Users are advised to upgrade. There are no known workarounds to remediate this vulnerability without upgrading to the patched version. 

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-670
cwe_name: Always-Incorrect Control Flow Implementation
description: The code contains a control flow path that does not reflect the algorithm that the path is intended to implement, leading to incorrect behavior any time this path is navigated.
extended_description: This weakness captures cases in which a particular code segment is always incorrect with respect to the algorithm that it is implementing. For example, if a C programmer intends to include multiple statements in a single block but does not include the enclosing braces (CWE-483), then the logic is always incorrect. This issue is in contrast to most weaknesses in which the code usually behaves correctly, except when it is externally manipulated in malicious ways.
         

Fix information:
repo_url: https://github.com/gofiber/fiber
commit_details: repo_url: https://github.com/gofiber/fiber
author: René Werner
author_date: 2023-09-06 13:20:33+02:00
msg: IsFromLocal corrected
repository: repo_url: https://github.com/gofiber/fiber
repo_name: gofiber/fiber
description: ⚡️ Express inspired web framework written in Go
repo_language: Go
owner: gofiber
file_changes: filename: ctx.go
change_type: ModificationType.MODIFY
diff: @@ -1887,12 +1887,12 @@ func (c *Ctx) IsProxyTrusted() bool {
 	return false
 }
 
-var localHosts = [...]string{"127.0.0.1", "0.0.0.0", "::1"}
+var localHosts = [...]string{"127.0.0.1", "::1"}
 
 // IsLocalHost will return true if address is a localhost address.
 func (*Ctx) isLocalHost(address string) bool {
 	for _, h := range localHosts {
-		if strings.Contains(address, h) {
+		if address == h {
 			return true
 		}
 	}
@@ -1901,9 +1901,5 @@ func (*Ctx) isLocalHost(address string) bool {
 
 // IsFromLocal will return true if request came from local.
 func (c *Ctx) IsFromLocal() bool {
-	ips := c.IPs()
-	if len(ips) == 0 {
-		ips = append(ips, c.IP())
-	}
-	return c.isLocalHost(ips[0])
+	return c.isLocalHost(c.fasthttp.RemoteIP().String())
 }

programming_language: Go
filename: ctx_test.go
change_type: ModificationType.MODIFY
diff: @@ -17,6 +17,7 @@ import (
 	"fmt"
 	"io"
 	"mime/multipart"
+	"net"
 	"net/http/httptest"
 	"net/url"
 	"os"
@@ -4918,57 +4919,126 @@ func Test_Ctx_GetReqHeaders(t *testing.T) {
 	})
 }
 
-// go test -run Test_Ctx_IsFromLocal
-func Test_Ctx_IsFromLocal(t *testing.T) {
+// go test -run Test_Ctx_IsFromLocal_X_Forwarded
+func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) {
 	t.Parallel()
-	// Test "0.0.0.0", "127.0.0.1" and "::1".
+	// Test unset X-Forwarded-For header.
 	{
 		app := New()
 		c := app.AcquireCtx(&fasthttp.RequestCtx{})
 		defer app.ReleaseCtx(c)
-		utils.AssertEqual(t, true, c.IsFromLocal())
+		// fasthttp returns "0.0.0.0" as IP as there is no remote address.
+		utils.AssertEqual(t, "0.0.0.0", c.IP())
+		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
-	// This is a test for "0.0.0.0"
+	// Test when setting X-Forwarded-For header to localhost "127.0.0.1"
 	{
 		app := New()
 		c := app.AcquireCtx(&fasthttp.RequestCtx{})
-		c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.0")
+		c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1")
 		defer app.ReleaseCtx(c)
-		utils.AssertEqual(t, true, c.IsFromLocal())
+		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
-
-	// This is a test for "127.0.0.1"
+	// Test when setting X-Forwarded-For header to localhost "::1"
 	{
 		app := New()
 		c := app.AcquireCtx(&fasthttp.RequestCtx{})
-		c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1")
+		c.Request().Header.Set(HeaderXForwardedFor, "::1")
 		defer app.ReleaseCtx(c)
-		utils.AssertEqual(t, true, c.IsFromLocal())
+		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
-
-	// This is a test for "localhost"
+	// Test when setting X-Forwarded-For to full localhost IPv6 address "0:0:0:0:0:0:0:1"
 	{
 		app := New()
 		c := app.AcquireCtx(&fasthttp.RequestCtx{})
+		c.Request().Header.Set(HeaderXForwardedFor, "0:0:0:0:0:0:0:1")
 		defer app.ReleaseCtx(c)
-		utils.AssertEqual(t, true, c.IsFromLocal())
+		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
-
-	// This is testing "::1", it is the compressed format IPV6 loopback address 0:0:0:0:0:0:0:1.
-	// It is the equivalent of the IPV4 address 127.0.0.1.
+	// Test for a random IP address.
 	{
 		app := New()
 		c := app.AcquireCtx(&fasthttp.RequestCtx{})
-		c.Request().Header.Set(HeaderXForwardedFor, "::1")
+		c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90")
 		defer app.ReleaseCtx(c)
-		utils.AssertEqual(t, true, c.IsFromLocal())
+		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
+}
 
+// go test -run Test_Ctx_IsFromLocal_RemoteAddr
+func Test_Ctx_IsFromLocal_RemoteAddr(t *testing.T) {
+	t.Parallel()
+
+	localIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP("127.0.0.1")})
+	localIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP("::1")})
+	localIPv6long := net.Addr(&net.TCPAddr{IP: net.ParseIP("0:0:0:0:0:0:0:1")})
+
+	zeroIPv4 := net.Addr(&net.TCPAddr{IP: net.IPv4zero})
+
+	someIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP("93.46.8.90")})
+	someIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")})
+
+	// Test for the case fasthttp remoteAddr is set to "127.0.0.1".
 	{
 		app := New()
-		c := app.AcquireCtx(&fasthttp.RequestCtx{})
-		c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90")
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(localIPv4)
+		c := app.AcquireCtx(fastCtx)
+		defer app.ReleaseCtx(c)
+
+		utils.AssertEqual(t, "127.0.0.1", c.IP())
+		utils.AssertEqual(t, true, c.IsFromLocal())
+	}
+	// Test for the case fasthttp remoteAddr is set to "::1".
+	{
+		app := New()
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(localIPv6)
+		c := app.AcquireCtx(fastCtx)
+		defer app.ReleaseCtx(c)
+		utils.AssertEqual(t, "::1", c.IP())
+		utils.AssertEqual(t, true, c.IsFromLocal())
+	}
+	// Test for the case fasthttp remoteAddr is set to "0:0:0:0:0:0:0:1".
+	{
+		app := New()
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(localIPv6long)
+		c := app.AcquireCtx(fastCtx)
+		defer app.ReleaseCtx(c)
+		// fasthttp should return "::1" for "0:0:0:0:0:0:0:1".
+		// otherwise IsFromLocal() will break.
+		utils.AssertEqual(t, "::1", c.IP())
+		utils.AssertEqual(t, true, c.IsFromLocal())
+	}
+	// Test for the case fasthttp remoteAddr is set to "0.0.0.0".
+	{
+		app := New()
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(zeroIPv4)
+		c := app.AcquireCtx(fastCtx)
+		defer app.ReleaseCtx(c)
+		utils.AssertEqual(t, "0.0.0.0", c.IP())
+		utils.AssertEqual(t, false, c.IsFromLocal())
+	}
+	// Test for the case fasthttp remoteAddr is set to "93.46.8.90".
+	{
+		app := New()
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(someIPv4)
+		c := app.AcquireCtx(fastCtx)
+		defer app.ReleaseCtx(c)
+		utils.AssertEqual(t, "93.46.8.90", c.IP())
+		utils.AssertEqual(t, false, c.IsFromLocal())
+	}
+	// Test for the case fasthttp remoteAddr is set to "2001:0db8:85a3:0000:0000:8a2e:0370:7334".
+	{
+		app := New()
+		fastCtx := &fasthttp.RequestCtx{}
+		fastCtx.SetRemoteAddr(someIPv6)
+		c := app.AcquireCtx(fastCtx)
 		defer app.ReleaseCtx(c)
+		utils.AssertEqual(t, "2001:db8:85a3::8a2e:370:7334", c.IP())
 		utils.AssertEqual(t, false, c.IsFromLocal())
 	}
 }

programming_language: Go