Skip to content

fix(extractors): reject a regex group the pattern cannot produce - #7612

Open
DPS0340 wants to merge 1 commit into
projectdiscovery:devfrom
DPS0340:fix/regex-group-out-of-range
Open

fix(extractors): reject a regex group the pattern cannot produce#7612
DPS0340 wants to merge 1 commit into
projectdiscovery:devfrom
DPS0340:fix/regex-group-out-of-range

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Closes #7611.

The bug

A regex extractor whose group exceeds the pattern's capture count compiles, runs, and silently extracts nothing — ExtractRegex skips every submatch because FindAllStringSubmatch returns exactly NumSubexp()+1 entries. Same silent-failure class as the negative group already rejected two lines above.

It affects shipped templates

Scanned nuclei-templates: 13,451 yaml files, 2,100 regex extractors with group > 0, 4 affected. That ratio matters — it says this is a defect, not behaviour templates depend on.

The clearest is smtp-credentials-exposure.yaml, where two patterns share one group: 1 and only the second captures:

regex:
  - smtp_username":".*"                    # 0 capture groups
  - <smtp_username>(.*)</smtp_username>    # 1 capture group

Against a body containing both, it extracts map[bob:{}]the JSON credential is silently missed, and has been since the template was written.

Two details worth flagging in review

Validation runs on the cached branch too. The compile path short-circuits on a cache hit:

if cached, err := cache.Regex().GetIFPresent(regex); err == nil && cached != nil {
    e.regexCompiled = append(e.regexCompiled, cached)
    continue
}

Checking only the freshly-compiled branch would let a pattern through unvalidated as soon as any other template had compiled it first. TestExtractor_CompileValidatesGroupOnCachedRegex covers exactly this, and I verified it catches a half-fix: removing only the cached-branch check still fails that one test.

The fuzz harness needed a change. It picks a group index independently of its patterns, and its own defaults include the group-less https?://[^\s"']+, so it would now fail at compile time on inputs it deliberately generates rather than exercising extraction. It clamps the group to what the narrowest pattern captures. I'd rather point this out than have it look like an unexplained edit to test scaffolding.

Verification

go test ./pkg/operators/...   all ok
go vet ./pkg/operators/extractors/   clean
gofmt   clean

Tests fail without the fix — removing both call sites gives 2 failures; removing only the cached-branch check gives 1.

Follow-up, not included here

The four templates need their group corrected (or the non-capturing pattern amended) in nuclei-templates. I've left that out of this PR since it's a different repo, but they aren't extracting what they claim to today and I'm happy to send it if useful.


Separately: the red Tests (windows-latest) on my other PR (#7605) is a goleveldb crash that also reproduces on #7598, which isn't mine — details in that thread. Mentioning it here only because this PR may show the same failure.

Summary by CodeRabbit

  • Bug Fixes

    • Invalid regex capture group selections are now rejected during extractor configuration instead of producing empty or misleading extraction results.
    • Validation is consistently applied, including when compiled regular expressions are reused from cache.
    • Valid capture groups continue to extract values as expected.
  • Tests

    • Added coverage for valid, invalid, and cached regular expression capture group scenarios.
    • Improved fuzz testing resilience by constraining generated capture group selections to supported ranges.

ExtractRegex skips any submatch where len(match) < RegexGroup+1, and
FindAllStringSubmatch returns exactly NumSubexp()+1 entries. So a group
index beyond the pattern's capture count can never match: the template
compiles, runs, and silently extracts nothing. An empty result is
indistinguishable from 'the pattern did not match the target', which is
the same silent-failure class as the negative-group case already
rejected two lines above.

Scanning nuclei-templates (13451 files, 2100 regex extractors with
group > 0) finds 4 real instances, and they look like genuine bugs
rather than false positives. The clearest is
smtp-credentials-exposure.yaml, where two patterns share one group: 1:

    regex:
      - smtp_username":".*"                        <- 0 groups
      - <smtp_username>(.*)</smtp_username>       <- 1 group

Against a body containing both forms it extracts only the XML one; the
JSON credential is missed with no indication. That template has been
half-working since it was written.

Validation runs on the cached-regex branch too. The compile path
short-circuits on a cache hit, so checking only the freshly-compiled
branch would let the same pattern through unvalidated as soon as any
other template had already compiled it.

The fuzz harness picks a group index independently of its patterns (its
own defaults include the group-less https?://[^\s"']+), so it now
clamps the group to what the narrowest pattern captures -- otherwise it
would fail at compile time on inputs it deliberately generates, instead
of exercising extraction.

Tests fail without the fix: removing both call sites gives 2 failures,
and removing only the cached-branch check still fails the cache test.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2934b433-a2ca-456c-b0c0-755474031f40

📥 Commits

Reviewing files that changed from the base of the PR and between 437897d and 2766609.

📒 Files selected for processing (3)
  • pkg/operators/extractors/compile.go
  • pkg/operators/extractors/extract_test.go
  • pkg/operators/extractors/fuzz_harness.go

Walkthrough

Regex extractor compilation now rejects positive group indexes beyond a pattern’s capture count, including cached regexes. Fuzz-generated extractors also clamp positive group indexes to supported capture counts.

Changes

Regex Group Safety

Layer / File(s) Summary
Compile-time regex group validation
pkg/operators/extractors/compile.go, pkg/operators/extractors/extract_test.go
CompileExtractors validates configured groups against NumSubexp() for cached and newly compiled regexes, with tests for invalid, valid, and cached paths.
Fuzz extractor group clamping
pkg/operators/extractors/fuzz_harness.go
Fuzz candidates clamp positive groups to the minimum capture count among compilable patterns, or use zero when no patterns compile.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Extractor
  participant CompileExtractors
  participant RegexCache
  Extractor->>CompileExtractors: compile regex extractor
  CompileExtractors->>RegexCache: load or compile regexp.Regexp
  RegexCache-->>CompileExtractors: compiled regex
  CompileExtractors->>CompileExtractors: validate RegexGroup against NumSubexp()
  CompileExtractors-->>Extractor: compiled extractor or error
Loading

Possibly related PRs

Poem

I’m a bunny guarding groups in line,
No hidden capture slips past mine.
Cached or fresh, the checks now bloom,
Fuzzed regexes find the room.
Bad indexes hop away—
Safe extractions win the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: rejecting regex groups the pattern cannot produce.
Linked Issues check ✅ Passed The PR implements the requested compile-time rejection for out-of-range regex groups, including the cached-regex path.
Out of Scope Changes check ✅ Passed The fuzz harness adjustment is support code for the same validation and stays within the issue's scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up with end-to-end evidence, plus a correction to something implied in the description.

The four templates are now fixed upstream: projectdiscovery/nuclei-templates#16663. After that lands the scan reports 0 of 2,100 regex extractors with an out-of-range group, so this check should not reject anything in the official set.

This PR alone does not surface the error through -validate. I built nuclei with only this change and ran the deliberately-broken template — it still reported success:

$ nuclei -validate -t probe.yaml     # group: 5 on a 1-group pattern
[INF] All templates validated successfully

That is not a fault in this change; it is #7602, which #7603 fixes. Applied together they compose exactly as intended:

$ nuclei -validate -t probe.yaml     # this PR + #7603
[ERR] Error occurred parsing template probe.yaml: could not compile request:
      could not compile operators: could not compile extractor:
      regex extractor group 5 is out of range for pattern "(\d+)",
      which has 1 capture group(s)
[FTL] Could not validate templates: errors occurred during template validation

And against the real templates, before/after:

before:  regex extractor group 1 is out of range for pattern
         "Oracle Containers for J2EE 10g \(.*\)", which has 0 capture group(s)
after:   All templates validated successfully

So the useful ordering is #7603 first (or alongside), otherwise this check only fires at scan time rather than at validation time. They are independent and each stands alone — I'd rather flag the interaction than have it discovered in review.

Cherry-picked #7603 onto this branch locally to confirm they merge cleanly; no conflicts.

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Correcting something I should have flagged when I opened this: the red functional tests are mine, not infrastructure. I pointed at the goleveldb crash on #7605 in the description and left it there, which reads as if the CI on this PR were the same story. It is not.

--- FAIL: TestFunctionalComparison/039_-tc_contains(extractor_type,'regex')
    functional_test.go:112: release loaded 1947 templates but current loaded 1944
--- FAIL: TestFunctionalComparison/040_-tc_contains(http_method,'GET')
    functional_test.go:112: release loaded 6893 templates but current loaded 6890

Three fewer templates, on all three platforms, deterministically. That is exactly the count this PR rejects: 4 patterns across 3 files.

oracle-containers-panel.yaml         group=1  'Oracle Containers for J2EE 10g \(.*\)'   0 groups
smtp-credentials-exposure.yaml       group=1  'smtp_username":".*"'                     0 groups
smtp-credentials-exposure.yaml       group=1  'smtp_password":".*"'                     0 groups
springboot-x-application-context.yaml group=1 '^X-Application-Context:\s*\S.+$'          0 groups

Re-scanned against current nuclei-templates: 13,391 yaml files, 2,084 regex extractors with group > 0, those 4 rejected. So TestFunctionalComparison cannot pass while those templates exist, because it asserts the release and dev binaries load an identical count.

The three templates are broken today. Running what ExtractRegex actually does:

oracle-containers-panel     matches body? true   extracted: map[]
springboot-x-app-context    matches body? true   extracted: map[]
smtp json cred              matches body? true   extracted: map[]

Each matches and extracts nothing, because FindAllStringSubmatch returns NumSubexp()+1 entries and group: 1 is always out of range. They have never worked.

So the disagreement is not about whether these are broken. It is about what a broken extractor should cost, and that is your call, not mine:

A. Fix the templates first. They are wrong regardless of this PR, and I would send that separately. Once nuclei-templates is corrected, this merges green. It leaves a window where dev rejects templates that the released binary still ships.

B. Warn instead of erroring. Log the mismatch at compile time and let the extractor load. The count stays equal, CI passes, and the silent failure becomes visible without being fatal. The cost is that it diverges from the negative-group check two lines above, which errors — I followed that precedent deliberately, but you may prefer consistency in the other direction.

C. Not worth it. If a template that silently extracts nothing is acceptable, close this. I would rather that than leave it open as noise.

I lean B, and I did not open with it because A was the more principled fix and I did not check what it did to TestFunctionalComparison before opening. That was my mistake — the failing job was on the PR from the start and I attributed it to the wrong cause. Happy to push whichever you pick.

Worth separating: #7605 and #7603 do not touch this code path and their CI is unrelated (#7605's Windows failure reproduces on #7598, which is not mine).

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Option A is now on the table rather than hypothetical: nuclei-templates#16665 fixes all three templates. Four changed lines, verified against a running target with nuclei built from dev:

before  [oracle-containers-panel:word-1]   [info] http://127.0.0.1:8732/
after   [oracle-containers-panel:word-1]   [info] http://127.0.0.1:8732/ ["10.1.3.5.0"]

before  [smtp-credentials-exposure:dsl-1]  [high] http://127.0.0.1:8732/
after   [smtp-credentials-exposure:dsl-1]  [high] http://127.0.0.1:8732/ ["Username: admin@example.com","PASSWORD: s3cr3t"]

Re-scanned after the change: 13,451 yaml files, 2,100 regex extractors with group > 0, 0 would be rejected. So once that merges, TestFunctionalComparison on this PR should go green on its own.

That leaves the ordering question, which is still yours: merging this before the templates land means the dev binary rejects templates the released binary still ships. If you would rather not have that window at all, B (warn instead of error) avoids it entirely and I will push it on request.

The choice between them is a real judgement call about what a broken extractor should cost, and I do not think it is mine to make. Either way the templates are worth fixing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do NOT touch the harness.

The whole point of the fix is to make the harness actually work. Leave it alone.

Akokonunes pushed a commit to projectdiscovery/nuclei-templates that referenced this pull request Jul 27, 2026
Each of these declares `group: 1` on a pattern with zero capture groups.
`ExtractRegex` reads `FindAllStringSubmatch`, which returns exactly
`NumSubexp()+1` entries per match, so index 1 is always out of range: the
template matches, reports, and extracts nothing.

Verified against a local server with nuclei built from dev. Before:

  [oracle-containers-panel:word-1] [http] [info] http://127.0.0.1:8732/
  [smtp-credentials-exposure:dsl-1] [http] [high] http://127.0.0.1:8732/

After:

  [oracle-containers-panel:word-1] ... ["10.1.3.5.0"]
  [smtp-credentials-exposure:dsl-1] ... ["Username: admin@example.com","PASSWORD: s3cr3t"]

smtp-credentials-exposure is the one that matters most: the JSON pattern
sits beside an XML pattern that does capture, so on a JSON body the
template fires and silently omits the credential it exists to report.
`[^"]*` rather than `.*` so a value cannot run past its closing quote into
the next field.

The other two extract a version string and a context path, both empty today.

Scan of the tree: 13,451 yaml files, 2,100 regex extractors with group > 0,
these 3 files (4 patterns) affected. Nothing else changes.

Found while adding a compile-time check for this in nuclei
(projectdiscovery/nuclei#7612).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] regex extractor group beyond the pattern capture count silently extracts nothing

2 participants