mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-03 08:28:21 +08:00
Compare commits
6 Commits
dc66825adf
...
59bc5a1e4e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59bc5a1e4e | ||
|
|
a9b0085e40 | ||
|
|
9b3d7f632a | ||
|
|
a61c9c6f6d | ||
|
|
d26a65283b | ||
|
|
cf959d79ff |
10
.github/workflows/gin.yml
vendored
10
.github/workflows/gin.yml
vendored
@ -54,17 +54,17 @@ jobs:
|
||||
TESTTAGS: ${{ matrix.test-tags }}
|
||||
GOPROXY: https://proxy.golang.org
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
|
||||
53
tree.go
53
tree.go
@ -85,16 +85,37 @@ func (n *node) addChild(child *node) {
|
||||
}
|
||||
|
||||
func countParams(path string) uint16 {
|
||||
var n uint16
|
||||
s := bytesconv.StringToBytes(path)
|
||||
n += uint16(bytes.Count(s, strColon))
|
||||
n += uint16(bytes.Count(s, strStar))
|
||||
return n
|
||||
colons := bytes.Count(s, strColon)
|
||||
stars := bytes.Count(s, strStar)
|
||||
total := colons + stars
|
||||
// Cap at max uint16 to prevent overflow
|
||||
if total > 0xFFFF {
|
||||
return 0xFFFF
|
||||
}
|
||||
return uint16(total)
|
||||
}
|
||||
|
||||
func countSections(path string) uint16 {
|
||||
s := bytesconv.StringToBytes(path)
|
||||
return uint16(bytes.Count(s, strSlash))
|
||||
count := bytes.Count(s, strSlash)
|
||||
// Cap at max uint16 to prevent overflow
|
||||
if count > 0xFFFF {
|
||||
return 0xFFFF
|
||||
}
|
||||
return uint16(count)
|
||||
}
|
||||
|
||||
// unescapePathValue unescapes a path parameter value if unescape is enabled.
|
||||
// Only unescapes if the value contains percent-encoded characters or plus signs.
|
||||
// This prevents double unescaping and potential path traversal vulnerabilities.
|
||||
func unescapePathValue(val string, unescape bool) string {
|
||||
if unescape && strings.ContainsAny(val, "%+") {
|
||||
if v, err := url.QueryUnescape(val); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
type nodeType uint8
|
||||
@ -519,16 +540,7 @@ walk: // Outer loop for walking the tree
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
val := path[:end]
|
||||
if unescape {
|
||||
// Only unescape if the value contains percent-encoded characters or plus signs
|
||||
// This prevents double unescaping and potential path traversal
|
||||
if strings.ContainsAny(val, "%+") {
|
||||
if v, err := url.QueryUnescape(val); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
}
|
||||
val := unescapePathValue(path[:end], unescape)
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[1:],
|
||||
Value: val,
|
||||
@ -576,16 +588,7 @@ walk: // Outer loop for walking the tree
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
val := path
|
||||
if unescape {
|
||||
// Only unescape if the value contains percent-encoded characters or plus signs
|
||||
// This prevents double unescaping and potential path traversal
|
||||
if strings.ContainsAny(path, "%+") {
|
||||
if v, err := url.QueryUnescape(path); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
}
|
||||
val := unescapePathValue(path, unescape)
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[2:],
|
||||
Value: val,
|
||||
|
||||
83
tree_test.go
83
tree_test.go
@ -94,6 +94,26 @@ func TestCountParams(t *testing.T) {
|
||||
if countParams(strings.Repeat("/:param", 256)) != 256 {
|
||||
t.Fail()
|
||||
}
|
||||
// Test overflow protection - should cap at max uint16 (0xFFFF = 65535)
|
||||
// Create a path with more than 65535 params (colons + stars)
|
||||
// Need 65536+ colons to trigger the overflow check
|
||||
overflowPath := strings.Repeat(":", 70000) // 70000 colons
|
||||
if countParams(overflowPath) != 0xFFFF {
|
||||
t.Errorf("countParams overflow protection failed: expected 0xFFFF, got %d", countParams(overflowPath))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountSections(t *testing.T) {
|
||||
if countSections("/path/to/resource") != 3 {
|
||||
t.Fail()
|
||||
}
|
||||
// Test overflow protection - should cap at max uint16 (0xFFFF = 65535)
|
||||
// Create a path with more than 65535 slashes
|
||||
// Need 65536+ slashes to trigger the overflow check
|
||||
overflowPath := strings.Repeat("/", 70000) // 70000 slashes
|
||||
if countSections(overflowPath) != 0xFFFF {
|
||||
t.Errorf("countSections overflow protection failed: expected 0xFFFF, got %d", countSections(overflowPath))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeAddAndGet(t *testing.T) {
|
||||
@ -377,16 +397,16 @@ func TestSecureParameterHandling(t *testing.T) {
|
||||
checkRequests(t, tree, testRequests{
|
||||
// Normal case - single encoding works as expected
|
||||
{"/info/user%2Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user/profile"}}},
|
||||
|
||||
|
||||
// Double encoding - should only decode once
|
||||
{"/info/user%252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user%2Fprofile"}}},
|
||||
|
||||
|
||||
// Triple encoding - should only decode once
|
||||
{"/info/user%25252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "user%252Fprofile"}}},
|
||||
|
||||
|
||||
// Mixed encoding - should only decode once
|
||||
{"/info/%2Fuser%252Fprofile", false, "/info/:user", Params{Param{Key: "user", Value: "/user%2Fprofile"}}},
|
||||
|
||||
|
||||
// No encoding - should pass through unchanged
|
||||
{"/info/user", false, "/info/:user", Params{Param{Key: "user", Value: "user"}}},
|
||||
}, unescape)
|
||||
@ -396,16 +416,16 @@ func TestSecureParameterHandling(t *testing.T) {
|
||||
checkRequests(t, tree, testRequests{
|
||||
// Normal case - single encoding works as expected
|
||||
{"/files/path%2Fto%2Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path/to/file.txt"}}},
|
||||
|
||||
|
||||
// Double encoding - should only decode once
|
||||
{"/files/path%252Fto%252Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path%2Fto%2Ffile.txt"}}},
|
||||
|
||||
|
||||
// Triple encoding - should only decode once
|
||||
{"/files/path%25252Fto%25252Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/path%252Fto%252Ffile.txt"}}},
|
||||
|
||||
|
||||
// Mixed encoding - should only decode once
|
||||
{"/files/%2Fpath%252Fto%2Ffile.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "//path%2Fto/file.txt"}}},
|
||||
|
||||
|
||||
// No encoding - should pass through unchanged
|
||||
{"/files/normal/file.txt", false, "/files/*filepath", Params{Param{Key: "filepath", Value: "/normal/file.txt"}}},
|
||||
}, unescape)
|
||||
@ -498,3 +518,50 @@ func TestTreeChildConflict(t *testing.T) {
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
||||
func TestWildcardConflictWithStringsCut(t *testing.T) {
|
||||
// Test the strings.Cut usage in wildcard conflict detection (line 258 in tree.go)
|
||||
tree := &node{}
|
||||
|
||||
// Add a route with a wildcard parameter
|
||||
tree.addRoute("/user/:name", fakeHandler("/user/:name"))
|
||||
|
||||
// Try to add a conflicting route that will trigger the strings.Cut path
|
||||
// This should panic with a wildcard conflict
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute("/user/:id/profile", fakeHandler("/user/:id/profile"))
|
||||
})
|
||||
|
||||
if recv == nil {
|
||||
t.Error("Expected panic for wildcard conflict, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatchAllConflictWithStringsCut(t *testing.T) {
|
||||
// Test the strings.Cut usage in catch-all conflict detection (line 382 in tree.go)
|
||||
tree := &node{}
|
||||
|
||||
// Add a route with a path segment
|
||||
tree.addRoute("/files/list", fakeHandler("/files/list"))
|
||||
|
||||
// Try to add a catch-all route that conflicts
|
||||
// This should panic with a catch-all conflict
|
||||
recv := catchPanic(func() {
|
||||
tree.addRoute("/files/*filepath", fakeHandler("/files/*filepath"))
|
||||
})
|
||||
|
||||
if recv == nil {
|
||||
t.Error("Expected panic for catch-all conflict, but got none")
|
||||
}
|
||||
|
||||
// Also test with an empty children case to cover line 382 when len(n.children) == 0
|
||||
tree2 := &node{}
|
||||
tree2.addRoute("/docs/", fakeHandler("/docs/"))
|
||||
|
||||
recv2 := catchPanic(func() {
|
||||
tree2.addRoute("/docs/*page", fakeHandler("/docs/*page"))
|
||||
})
|
||||
|
||||
if recv2 == nil {
|
||||
t.Error("Expected panic for catch-all conflict with empty children, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user