CVE-2023-41338: Always-Incorrect Control Flow Implementation (CWE-670)
value: 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. 
CWE 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.
CVSS v3 base score: 5.3
Published: 2023-09-08T19:15Z
Repository: https://github.com/gofiber/fiber
Language: Go
Files changed: ctx.go, ctx_test.go
Commit date: 2023-09-06 13:20:33 +0200
Commit message: IsFromLocal corrected

Diff: diff --git a/ctx.go b/ctx.go
index ddb1e61d..f409250a 100644
--- a/ctx.go
+++ b/ctx.go
@@ -1885,25 +1885,21 @@ 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
 		}
 	}
 	return false
 }
 
 // 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())
 }
diff --git a/ctx_test.go b/ctx_test.go
index 4ea1e877..52c62fe5 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -15,10 +15,11 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
 	"io"
 	"mime/multipart"
+	"net"
 	"net/http/httptest"
 	"net/url"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -4916,61 +4917,130 @@ func Test_Ctx_GetReqHeaders(t *testing.T) {
 		"Foo":          "bar",
 		"Test":         "Hello, World 👋!",
 	})
 }
 
-// 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())
 	}
 }
 
 // go test -run Test_Ctx_RepeatParserWithSameStruct -v

Vulnerable code: var localHosts = [...]string{"127.0.0.1", "0.0.0.0", "::1"}
		if strings.Contains(address, h) {
	ips := c.IPs()
	if len(ips) == 0 {
		ips = append(ips, c.IP())
	}
	return c.isLocalHost(ips[0])
// go test -run Test_Ctx_IsFromLocal
func Test_Ctx_IsFromLocal(t *testing.T) {
	// Test "0.0.0.0", "127.0.0.1" and "::1".
		utils.AssertEqual(t, true, c.IsFromLocal())
	// This is a test for "0.0.0.0"
		c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.0")
		utils.AssertEqual(t, true, c.IsFromLocal())

	// This is a test for "127.0.0.1"
		c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1")
		utils.AssertEqual(t, true, c.IsFromLocal())

	// This is a test for "localhost"
		utils.AssertEqual(t, true, 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.
		c.Request().Header.Set(HeaderXForwardedFor, "::1")
		utils.AssertEqual(t, true, c.IsFromLocal())
		c := app.AcquireCtx(&fasthttp.RequestCtx{})
		c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90")
Fixed code: var localHosts = [...]string{"127.0.0.1", "::1"}
		if address == h {
	return c.isLocalHost(c.fasthttp.RemoteIP().String())
	"net"
// go test -run Test_Ctx_IsFromLocal_X_Forwarded
func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) {
	// Test unset X-Forwarded-For header.
		// 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())
	// Test when setting X-Forwarded-For header to localhost "127.0.0.1"
		c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1")
		utils.AssertEqual(t, false, c.IsFromLocal())
	// Test when setting X-Forwarded-For header to localhost "::1"
		c.Request().Header.Set(HeaderXForwardedFor, "::1")
		utils.AssertEqual(t, false, c.IsFromLocal())
	// Test when setting X-Forwarded-For to full localhost IPv6 address "0:0:0:0:0:0:0:1"
		c.Request().Header.Set(HeaderXForwardedFor, "0:0:0:0:0:0:0:1")
		utils.AssertEqual(t, false, c.IsFromLocal())
	// Test for a random IP address.
		c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90")
		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".
		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)
		utils.AssertEqual(t, "2001:db8:85a3::8a2e:370:7334", c.IP())