Skip to content

fix(fuzz): stabilize path fuzzing for numeric segments#7152

Open
a638011 wants to merge 4 commits intoprojectdiscovery:devfrom
a638011:fix/6398-combined-root-causes
Open

fix(fuzz): stabilize path fuzzing for numeric segments#7152
a638011 wants to merge 4 commits intoprojectdiscovery:devfrom
a638011:fix/6398-combined-root-causes

Conversation

@a638011
Copy link

@a638011 a638011 commented Mar 8, 2026

Proposed Changes

Fixes #6398 — path-based fuzzing still skips numeric segments on dev under some runs.

This keeps the fix minimal and addresses the remaining root causes together:

  1. Deterministic path segment iteration

    • Path.Parse() used a plain Go map, so segment iteration order was unstable.
    • Switch it to ordered storage so /user/55/profile is always iterated as user, 55, profile.
  2. Preserve the original path across rebuilds

    • retryablehttp.Request.Clone() shares the underlying URL pointer, so UpdateRelPath() mutates the original request path too.
    • Snapshot the original path at parse time and rebuild from that immutable value.
  3. Frequency tracking uses the effective path parameter

    • Frequency dedup checked the numeric index ("2") instead of the effective path value ("55").
    • Use actualParameter so different numeric segments do not collide on the same path position.

Proof

Reproduced on current dev:

  • Path.Parse()+Iterate() on /user/55/profile produced different orders across runs, including:
    • [1=user 2=55 3=profile]
    • [2=55 3=profile 1=user]
    • [3=profile 1=user 2=55]
  • Path.Rebuild() mutated the original request path after cloning:
    • before: /user/55/profile
    • after rebuilding a clone: original request became /user/55/profile OR True
  • frequency dedup still keyed on the raw numeric path index instead of the effective segment value.

Focused regression tests added:

go test ./pkg/fuzz/component -run 'TestURLComponent|TestPathComponent' -count=1 -v
go test ./pkg/fuzz -run 'TestExecWithInputUsesActualParameterForFrequency|TestRuleMatchKeyOrValue|TestEvaluateVariables|TestEvaluateVarsWithInteractsh_RaceCondition' -count=1 -v
go test ./pkg/fuzz/... -count=1

All passed locally.

Summary

  • ordered path storage in pkg/fuzz/component/path.go
  • immutable original-path snapshot for rebuilds
  • frequency dedup fixed to use effective path values
  • focused regressions for:
    • deterministic path iteration
    • rebuilds using original path snapshot
    • frequency tracking on actual path values

Checklist

  • PR created against dev
  • Focused regression tests added
  • Relevant fuzz package tests passed locally

/claim #6398

Summary by CodeRabbit

  • New Features

    • Path reconstruction now preserves deterministic segment order and keeps an immutable original-path snapshot for rebuilds.
  • Bug Fixes

    • Rebuilds preserve original and empty segments and avoid mutating the original request URL.
    • Parameter frequency tracking now keys on actual substituted values, preventing incorrect skips.
  • Tests

    • Added tests for path ordering, rebuild snapshot behavior, empty-segment preservation, and frequency-tracking with substituted parameters.

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 8, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Enhanced inline documentation in path.go explaining the deep-copy logic for URL cloning to prevent mutation of original requests
  • Minor formatting and structural refinements in test assertions for path_test.go with no behavioral changes

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d909af66-f62e-48a8-9a30-3cac132730b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1d75792 and 5360d9a.

📒 Files selected for processing (2)
  • pkg/fuzz/component/path.go
  • pkg/fuzz/component/path_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/fuzz/component/path_test.go

Walkthrough

Introduces an immutable originalPath snapshot in the Path component, switches path segments/values to ordered maps with 1-based indexing to preserve insertion order and empty segments, deep-copies request URLs on Clone/Rebuild, and changes execWithInput to use the actual (possibly substituted) parameter value for frequency checks.

Changes

Cohort / File(s) Summary
Path component updates
pkg/fuzz/component/path.go, pkg/fuzz/component/path_test.go
Added originalPath string; segments and parsed values now use ordered maps with a 1-based segmentIndex; Rebuild uses originalPath, preserves empty segments, uses direct Get assertions, and deep-copies URL during Clone/Rebuild; tests added for deterministic ordering, snapshot-based rebuild, and empty-segment preservation.
Frequency keying change
pkg/fuzz/parts.go, pkg/fuzz/parts_frequency_test.go
execWithInput now evaluates frequency-based skipping using actualParameter (the substituted/real value) instead of the original parameter index/key; added test to assert frequency tracking and callback invocation use the actual parameter value.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through segments, one by one,
I numbered each step and kept the sun,
A snapshot held the path I knew,
Rebuilds follow traces true and new,
Empty steps kept, bugs chased—recipe done.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main purpose: fixing path fuzzing for numeric segments by stabilizing iteration order.
Linked Issues check ✅ Passed All coding requirements from #6398 are implemented: deterministic path iteration via ordered storage, immutable original-path snapshot for rebuilds, and frequency dedup keyed on effective values.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the three root causes identified in #6398 with no extraneous modifications introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/fuzz/component/path.go (2)

95-123: ⚠️ Potential issue | 🟠 Major

Rebuild still collapses empty path segments.

Using originalPath fixes ordering, but the continue on empty segments still rewrites paths like /a//b/ into /a/b and drops trailing slashes. That changes the target route, so path fuzzing is still not layout-preserving for these inputs.

Suggested fix
 	for i := 1; i < len(originalSplitted); i++ {
 		originalSegment := originalSplitted[i]
 		if originalSegment == "" {
-			// Skip empty segments
-			continue
+			rebuiltSegments = append(rebuiltSegments, "")
+			continue
 		}
 
 		// Check if we have a replacement for this segment
 		key := strconv.Itoa(segmentIndex)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/fuzz/component/path.go` around lines 95 - 123, The rebuild loop currently
skips empty segments (the `continue`), which collapses consecutive slashes and
drops trailing slash; instead preserve empty segments by removing the
`continue`: when originalSegment == "" append an empty string to rebuiltSegments
(so `/a//b/` becomes `["", "a", "", "b", ""]`) and still advance segmentIndex
and the rest of the logic so q.value.parsed.Get(strconv.Itoa(segmentIndex))
stays in sync; update the loop that references originalSplitted,
rebuiltSegments, segmentIndex, and q.value.parsed.Get to append "" for empty
segments rather than skipping them.

138-143: ⚠️ Potential issue | 🟠 Major

Ensure Rebuild() doesn't mutate the base request by deep-copying the URL before mutation.

The current implementation clones the request but may still mutate q.req if retryablehttp.Request.Clone() shares the underlying URL pointer. The test TestPathComponent_RebuildUsesOriginalPathSnapshot (lines 151-171) only verifies that the rebuilt path is correct, but doesn't assert that the original request remains unchanged after Rebuild() completes. Add an assertion to verify req.Path stays unchanged after rebuilding, and consider deep-copying the URL struct before calling UpdateRelPath() to guarantee immutability of the original request object.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/fuzz/component/path.go` around lines 138 - 143, The Rebuild()
implementation currently clones q.req but may still share the underlying URL;
fix by deep-copying the URL struct on the cloned request before calling
UpdateRelPath — e.g., after cloned := q.req.Clone(...), allocate a new url.URL,
copy *cloned.URL into it and assign cloned.URL = newURL so UpdateRelPath cannot
mutate q.req; preserve the existing RawPath fallback behavior (setting
cloned.RawPath = rebuiltPath on error). Also update the
TestPathComponent_RebuildUsesOriginalPathSnapshot test to assert that the
original req.Path (or original q.req.URL.Path) remains unchanged after calling
Rebuild().
🧹 Nitpick comments (1)
pkg/fuzz/parts_frequency_test.go (1)

34-41: Use the same target when seeding frequency state.

This only proves the old collision if both calls normalize to the same target. Marking "99" against /blog/99/post makes the test depend on normalizeTarget() stripping the path. Seeding with baseReq.String() keeps the regression focused on the parameter-key change itself.

Suggested test tightening
-	// Mark a different numeric path segment as frequent on the same target.
-	// With the old code, both values collided on the raw index key "2".
-	targetReq, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com/blog/99/post", nil)
-	require.NoError(t, err)
-	rule.options.FuzzParamsFrequency.MarkParameter("99", targetReq.String(), rule.options.TemplateID)
-
 	baseReq, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com/user/55/profile", nil)
 	require.NoError(t, err)
+	// Mark a different numeric path segment as frequent on the same target.
+	// With the old code, both values collided on the raw index key "2".
+	rule.options.FuzzParamsFrequency.MarkParameter("99", baseReq.String(), rule.options.TemplateID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/fuzz/parts_frequency_test.go` around lines 34 - 41, The test currently
marks the frequency using targetReq.String(), which makes the assertion depend
on normalizeTarget() behavior; instead create both requests (baseReq and
targetReq) and seed the frequency state using the same target string as the
later check by calling rule.options.FuzzParamsFrequency.MarkParameter("99",
baseReq.String(), rule.options.TemplateID) (ensure baseReq is constructed before
you call MarkParameter), so the test isolates the parameter-key collision logic
rather than path normalization—refer to targetReq, baseReq, MarkParameter and
normalizeTarget() when locating the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/fuzz/component/path.go`:
- Around line 95-123: The rebuild loop currently skips empty segments (the
`continue`), which collapses consecutive slashes and drops trailing slash;
instead preserve empty segments by removing the `continue`: when originalSegment
== "" append an empty string to rebuiltSegments (so `/a//b/` becomes `["", "a",
"", "b", ""]`) and still advance segmentIndex and the rest of the logic so
q.value.parsed.Get(strconv.Itoa(segmentIndex)) stays in sync; update the loop
that references originalSplitted, rebuiltSegments, segmentIndex, and
q.value.parsed.Get to append "" for empty segments rather than skipping them.
- Around line 138-143: The Rebuild() implementation currently clones q.req but
may still share the underlying URL; fix by deep-copying the URL struct on the
cloned request before calling UpdateRelPath — e.g., after cloned :=
q.req.Clone(...), allocate a new url.URL, copy *cloned.URL into it and assign
cloned.URL = newURL so UpdateRelPath cannot mutate q.req; preserve the existing
RawPath fallback behavior (setting cloned.RawPath = rebuiltPath on error). Also
update the TestPathComponent_RebuildUsesOriginalPathSnapshot test to assert that
the original req.Path (or original q.req.URL.Path) remains unchanged after
calling Rebuild().

---

Nitpick comments:
In `@pkg/fuzz/parts_frequency_test.go`:
- Around line 34-41: The test currently marks the frequency using
targetReq.String(), which makes the assertion depend on normalizeTarget()
behavior; instead create both requests (baseReq and targetReq) and seed the
frequency state using the same target string as the later check by calling
rule.options.FuzzParamsFrequency.MarkParameter("99", baseReq.String(),
rule.options.TemplateID) (ensure baseReq is constructed before you call
MarkParameter), so the test isolates the parameter-key collision logic rather
than path normalization—refer to targetReq, baseReq, MarkParameter and
normalizeTarget() when locating the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44e328bb-dd93-4163-8301-d1ebe8d34640

📥 Commits

Reviewing files that changed from the base of the PR and between b5cb4cc and 39fba3c.

📒 Files selected for processing (4)
  • pkg/fuzz/component/path.go
  • pkg/fuzz/component/path_test.go
  • pkg/fuzz/parts.go
  • pkg/fuzz/parts_frequency_test.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/fuzz/component/path.go`:
- Around line 118-121: The rebuild currently treats an explicit empty
replacement as "missing" because of the newValue != "" check; change the logic
in the rebuild loop (the branch using q.value.parsed.Get(key).(string),
rebuiltSegments and originalSegment) to only fall back to originalSegment when
the key is absent (i.e., when the type assertion/ok is false). In other words,
remove the newValue != "" guard so that when ok is true you append newValue even
if it's an empty string, and only append originalSegment when ok is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e348f6a-0511-4daa-9242-e4cb46d2f4a2

📥 Commits

Reviewing files that changed from the base of the PR and between 7a766a3 and 1d75792.

📒 Files selected for processing (2)
  • pkg/fuzz/component/path.go
  • pkg/fuzz/component/path_test.go

…y tests

- Remove newValue != "" guard to support explicit empty string replacements
- Clarify URL deep-copy with intermediate variable for better readability
- Add explicit assertions that original request path remains unchanged after Rebuild
- Addresses CodeRabbit review feedback on PR projectdiscovery#7152
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Fuzzing templates skips numeric path parts

1 participant