Fingerprint#match interpolation takes the wrong branch when an optional capture becomes "" instead of nil
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 86/100
Research direction
Start in lib/recog/fingerprint.rb around lines 112-130, where replacement interpolation branches on the captured value. Reproduce the issue with the LiteSpeed fingerprint and "LiteSpeed Enterprise" example, then verify that an empty optional capture receives the same '-' fallback as nil and that the resulting service.cpe23 value is correct.
Written by the indexing model from the issue text.
Description
Describe the bug
Fingerprint#match's interpolation logic in lib/recog/fingerprint.rb branches on whether a captured value is present:
replacements.each_pair do |replacement_k, replacement_vs|
replacement_vs.each do |replacement|
if result[replacement]
result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, result[replacement])
else
# ...falls back to '-' for cpe23 version fields...
result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, '-')
end
This assumes an unmatched optional capture group is always nil. That's true for a pattern like (.+)?, but not for (.*) — when the group doesn't participate in the match, (.+)? captures nil while (.*) captures "". Since "" is truthy in Ruby, the if result[replacement] branch is taken instead of the intended else fallback, and the raw (empty) capture gets interpolated instead of the - placeholder.
This matters because any XML pattern change that swaps an optional capturing group from (X+)? to (X*) — for example, as part of quieting Ruby 3.3's nested repeat operator warning will silently change this interpolation behavior for any fingerprint relying on the nil fallback, even though the regex itself still matches the same strings.
To Reproduce
Steps to reproduce the behavior:
- Use a fingerprint whose version capture is optional and currently written as
(.+)? - Match it against a string where that optional group doesn't participate (no version present)
- Note
result["service.version"]isnilandresult["service.cpe23"]correctly gets the-placeholder - Change the pattern's optional group from
(.+)?to(.*)(a seemingly equivalent rewrite - both match the exact same strings) - Re-run the same match —
result["service.version"]is now""instead ofnil, so theif result[replacement]branch is taken, andresult["service.cpe23"]ends up missing the-placeholder entirely
Code that reproduces the behavior:
# with (.+)? - correct
result["service.version"] #=> nil
result["service.cpe23"] #=> "cpe:/a:litespeedtech:litespeed_web_server:-"
# with (.*) - incorrect
result["service.version"] #=> ""
result["service.cpe23"] #=> "cpe:/a:litespeedtech:litespeed_web_server:"
Matcher that reproduces the behavior:
<fingerprint pattern="^LiteSpeed\/?([\d.]+)?(?: \S+)?">
<description>LiteSpeed</description>
<example>LiteSpeed Enterprise</example>
<param pos="0" name="service.vendor" value="LiteSpeed Technologies"/>
<param pos="0" name="service.product" value="LiteSpeed Web Server"/>
<param pos="1" name="service.version"/>
<param pos="0" name="service.cpe23" value="cpe:/a:litespeedtech:litespeed_web_server:{service.version}"/>
</fingerprint>
Matching this against "LiteSpeed Enterprise" (no version present) reproduces the behavior above.
Expected behavior
An unmatched optional capture should be treated the same for interpolation purposes whether the underlying regex produces nil or "" - i.e. the cpe23 value should get the - placeholder in both cases, since no version is actually present. Suggested fix: treat an empty captured string the same as a nil capture when deciding whether to interpolate:
if result[replacement] && !result[replacement].empty?
Environment
- Operating System: N/A (pure Ruby logic bug, not OS-dependent)
- Ruby Version: 3.3.8
- Recog Version: current
main(relevant code:lib/recog/fingerprint.rb, lines 112-130)
Additional context
Found while reviewing rapid7/recog#662, which proposed a blanket (.+)? → (.*) substitution across the XML database to quiet Ruby 3.3's "nested repeat operator" warning. That PR was closed in favor of rapid7/recog#682, which audits each pattern individually instead. This issue tracks the underlying fingerprint.rb bug separately, as requested by @cdelafuente-r7: https://github.com/rapid7/recog/issues/662#issuecomment-5207469937
Happy to submit a PR for this fix.
- Dominant language
- Ruby
- Stars
- 10
- Forks
- 9
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Similar issues
-
バグ
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
voxpupuli/puppet-epel#186 · 1 comment ·
-
external_created_at is no longer used for the message timestamp since the new message UI (v4.4.0) OpenBug Frontend
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
TheOdinProject/curriculum#31402 · 1 comment ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100