CVE-2022-21221: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: The package github.com/valyala/fasthttp before 1.34.0 are vulnerable to Directory Traversal via the ServeFile function, due to improper sanitization. It is possible to be exploited by using a backslash %5c character in the path. **Note:** This security issue impacts Windows users only.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2022-03-17T12:15Z
Repository: https://github.com/valyala/fasthttp
Language: Go
Files changed: strings.go, uri.go
Commit date: 2022-02-28 11:56:59 +0100
Commit message: Add windows support to normalizePath

This is probably still not 100% sure and there are still many bugs with
FS on windows. But it's a slight improvement.

Fixes #1226

Diff: diff --git a/strings.go b/strings.go
index e4fb08b..370e307 100644
--- a/strings.go
+++ b/strings.go
@@ -5,25 +5,29 @@ var (
 	defaultUserAgent   = []byte("fasthttp")
 	defaultContentType = []byte("text/plain; charset=utf-8")
 )
 
 var (
-	strSlash            = []byte("/")
-	strSlashSlash       = []byte("//")
-	strSlashDotDot      = []byte("/..")
-	strSlashDotSlash    = []byte("/./")
-	strSlashDotDotSlash = []byte("/../")
-	strCRLF             = []byte("\r\n")
-	strHTTP             = []byte("http")
-	strHTTPS            = []byte("https")
-	strHTTP10           = []byte("HTTP/1.0")
-	strHTTP11           = []byte("HTTP/1.1")
-	strColon            = []byte(":")
-	strColonSlashSlash  = []byte("://")
-	strColonSpace       = []byte(": ")
-	strCommaSpace       = []byte(", ")
-	strGMT              = []byte("GMT")
+	strSlash                    = []byte("/")
+	strSlashSlash               = []byte("//")
+	strSlashDotDot              = []byte("/..")
+	strSlashDotSlash            = []byte("/./")
+	strSlashDotDotSlash         = []byte("/../")
+	strBackSlashDotDot          = []byte(`\..`)
+	strBackSlashDotBackSlash    = []byte(`\.\`)
+	strSlashDotDotBackSlash     = []byte(`/..\`)
+	strBackSlashDotDotBackSlash = []byte(`\..\`)
+	strCRLF                     = []byte("\r\n")
+	strHTTP                     = []byte("http")
+	strHTTPS                    = []byte("https")
+	strHTTP10                   = []byte("HTTP/1.0")
+	strHTTP11                   = []byte("HTTP/1.1")
+	strColon                    = []byte(":")
+	strColonSlashSlash          = []byte("://")
+	strColonSpace               = []byte(": ")
+	strCommaSpace               = []byte(", ")
+	strGMT                      = []byte("GMT")
 
 	strResponseContinue = []byte("HTTP/1.1 100 Continue\r\n\r\n")
 
 	strExpect             = []byte(HeaderExpect)
 	strConnection         = []byte(HeaderConnection)
diff --git a/uri.go b/uri.go
index 62974df..38a431e 100644
--- a/uri.go
+++ b/uri.go
@@ -3,10 +3,11 @@ package fasthttp
 import (
 	"bytes"
 	"errors"
 	"fmt"
 	"io"
+	"path/filepath"
 	"strconv"
 	"sync"
 )
 
 // AcquireURI returns an empty URI instance from the pool.
@@ -632,10 +633,64 @@ func normalizePath(dst, src []byte) []byte {
 			return append(dst[:0], strSlash...)
 		}
 		b = b[:nn+1]
 	}
 
+	if filepath.Separator == '\\' {
+		// remove \.\ parts
+		b = dst
+		for {
+			n := bytes.Index(b, strBackSlashDotBackSlash)
+			if n < 0 {
+				break
+			}
+			nn := n + len(strSlashDotSlash) - 1
+			copy(b[n:], b[nn:])
+			b = b[:len(b)-nn+n]
+		}
+
+		// remove /foo/..\ parts
+		for {
+			n := bytes.Index(b, strSlashDotDotBackSlash)
+			if n < 0 {
+				break
+			}
+			nn := bytes.LastIndexByte(b[:n], '/')
+			if nn < 0 {
+				nn = 0
+			}
+			n += len(strSlashDotDotBackSlash) - 1
+			copy(b[nn:], b[n:])
+			b = b[:len(b)-n+nn]
+		}
+
+		// remove /foo\..\ parts
+		for {
+			n := bytes.Index(b, strBackSlashDotDotBackSlash)
+			if n < 0 {
+				break
+			}
+			nn := bytes.LastIndexByte(b[:n], '/')
+			if nn < 0 {
+				nn = 0
+			}
+			n += len(strBackSlashDotDotBackSlash) - 1
+			copy(b[nn:], b[n:])
+			b = b[:len(b)-n+nn]
+		}
+
+		// remove trailing \foo\..
+		n := bytes.LastIndex(b, strBackSlashDotDot)
+		if n >= 0 && n+len(strSlashDotDot) == len(b) {
+			nn := bytes.LastIndexByte(b[:n], '/')
+			if nn < 0 {
+				return append(dst[:0], strSlash...)
+			}
+			b = b[:nn+1]
+		}
+	}
+
 	return b
 }
 
 // RequestURI returns RequestURI - i.e. URI without Scheme and Host.
 func (u *URI) RequestURI() []byte {

Vulnerable code: 	strSlash            = []byte("/")
	strSlashSlash       = []byte("//")
	strSlashDotDot      = []byte("/..")
	strSlashDotSlash    = []byte("/./")
	strSlashDotDotSlash = []byte("/../")
	strCRLF             = []byte("\r\n")
	strHTTP             = []byte("http")
	strHTTPS            = []byte("https")
	strHTTP10           = []byte("HTTP/1.0")
	strHTTP11           = []byte("HTTP/1.1")
	strColon            = []byte(":")
	strColonSlashSlash  = []byte("://")
	strColonSpace       = []byte(": ")
	strCommaSpace       = []byte(", ")
	strGMT              = []byte("GMT")
Fixed code: 	strSlash                    = []byte("/")
	strSlashSlash               = []byte("//")
	strSlashDotDot              = []byte("/..")
	strSlashDotSlash            = []byte("/./")
	strSlashDotDotSlash         = []byte("/../")
	strBackSlashDotDot          = []byte(`\..`)
	strBackSlashDotBackSlash    = []byte(`\.\`)
	strSlashDotDotBackSlash     = []byte(`/..\`)
	strBackSlashDotDotBackSlash = []byte(`\..\`)
	strCRLF                     = []byte("\r\n")
	strHTTP                     = []byte("http")
	strHTTPS                    = []byte("https")
	strHTTP10                   = []byte("HTTP/1.0")
	strHTTP11                   = []byte("HTTP/1.1")
	strColon                    = []byte(":")
	strColonSlashSlash          = []byte("://")
	strColonSpace               = []byte(": ")
	strCommaSpace               = []byte(", ")
	strGMT                      = []byte("GMT")
	"path/filepath"
	if filepath.Separator == '\\' {
		// remove \.\ parts
		b = dst
		for {
			n := bytes.Index(b, strBackSlashDotBackSlash)
			if n < 0 {
				break
			}
			nn := n + len(strSlashDotSlash) - 1
			copy(b[n:], b[nn:])
			b = b[:len(b)-nn+n]
		}

		// remove /foo/..\ parts
		for {
			n := bytes.Index(b, strSlashDotDotBackSlash)
			if n < 0 {
				break
			}
			nn := bytes.LastIndexByte(b[:n], '/')
			if nn < 0 {
				nn = 0
			}
			n += len(strSlashDotDotBackSlash) - 1
			copy(b[nn:], b[n:])
			b = b[:len(b)-n+nn]
		}

		// remove /foo\..\ parts
		for {
			n := bytes.Index(b, strBackSlashDotDotBackSlash)
			if n < 0 {
				break
			}
			nn := bytes.LastIndexByte(b[:n], '/')
			if nn < 0 {
				nn = 0
			}
			n += len(strBackSlashDotDotBackSlash) - 1
			copy(b[nn:], b[n:])
			b = b[:len(b)-n+nn]
		}

		// remove trailing \foo\..
		n := bytes.LastIndex(b, strBackSlashDotDot)
		if n >= 0 && n+len(strSlashDotDot) == len(b) {
			nn := bytes.LastIndexByte(b[:n], '/')
			if nn < 0 {
				return append(dst[:0], strSlash...)
			}
			b = b[:nn+1]
		}
	}