diff --git a/internal/attack/incremental.go b/internal/attack/incremental.go index 8566f3b..4546a40 100644 --- a/internal/attack/incremental.go +++ b/internal/attack/incremental.go @@ -60,7 +60,7 @@ func findChannelIncrement(route string) (incrementalRoute, bool) { } pos += index - start, end, ok := firstNumberAfter(route, pos+len(pattern)) + start, end, ok := firstNumberAfterKey(route, pos+len(pattern)) if ok { num, width, parseOK := parseNumber(route, start, end) if parseOK { @@ -131,23 +131,41 @@ func parseNumber(route string, start, end int) (int, int, bool) { return num, len(value), true } -// firstNumberAfter returns the first numeric token after a given index. -func firstNumberAfter(route string, after int) (start, end int, ok bool) { +// firstNumberAfterKey returns the first numeric token after a keyword, limited to +// the current token and requiring an '=' delimiter (query param or path segment). +func firstNumberAfterKey(route string, after int) (start, end int, ok bool) { if after < 0 { after = 0 } + tokenEnd := len(route) for i := after; i < len(route); i++ { + if isTokenDelimiter(route[i]) { + tokenEnd = i + break + } + } + + relEq := strings.IndexByte(route[after:tokenEnd], '=') + searchStart := after + if relEq != -1 { + searchStart = after + relEq + 1 + } + for i := searchStart; i < tokenEnd; i++ { if !isDigit(route[i]) { + if relEq == -1 { + break + } continue } end := i + 1 - for end < len(route) && isDigit(route[end]) { + for end < tokenEnd && isDigit(route[end]) { end++ } return i, end, true } + return 0, 0, false } @@ -163,3 +181,12 @@ func buildIncrementedRoute(match incrementalRoute, number int) string { func isDigit(b byte) bool { return b >= '0' && b <= '9' } + +func isTokenDelimiter(b byte) bool { + switch b { + case '&', '/', '?', '#': + return true + default: + return false + } +} diff --git a/internal/attack/incremental_test.go b/internal/attack/incremental_test.go index 6c4a5df..a279897 100644 --- a/internal/attack/incremental_test.go +++ b/internal/attack/incremental_test.go @@ -66,6 +66,18 @@ func TestDetectIncrementalRoute_OverflowAtEndFallsBack(t *testing.T) { assert.Equal(t, "/bar999999999999999999999999999999", match.suffix) } +func TestDetectIncrementalRoute_ChannelKeywordShouldNotBindAcrossParams(t *testing.T) { + // The channel keyword should not bind to digits in other query parameters. + route := "/path?channelname=foo&version=12" + + match, ok := detectIncrementalRoute(route) + require.True(t, ok) + assert.False(t, match.isChannel) + assert.Equal(t, 12, match.number) + assert.Equal(t, "/path?channelname=foo&version=", match.prefix) + assert.Equal(t, "", match.suffix) +} + func TestBuildIncrementedRoute_ZeroPadding(t *testing.T) { match := incrementalRoute{ prefix: "/channel",