Regex Surprises: When gitleaks's Pattern Matching Breaks
Regular expressions with optional groups and alternative branches produce unexpected matches on edge case inputs, causing downstream failures.

The bottom line: Regular expressions with optional groups and alternative branches produce unexpected matches on edge case inputs, causing downstream failures. Always test regex against empty and minimal inputs.
The Problem
gitleaks/gitleaks issue #2121 exposes a subtle edge case in how regex handles boundary conditions with optional groups. The fix is only 1 line, but the pattern behind it applies across projects.
PR: https://github.com/gitleaks/gitleaks/pull/2163
Status: Submitted (awaiting review)
What Goes Wrong
Regular expressions with optional groups and alternatives behave unexpectedly when input matches multiple branches. The regex engine backtracks and may match unintended branches.
import re
# Edge case: optional group matches differently than expected
pattern = r'(foo)?(bar)?'
# Matches 'foobar', 'foo', 'bar', AND empty string!
result = re.match(pattern, '') # Matches: both groups omitted
Common failure patterns to watch for:
- Optional groups — A group like
(foo)?makes its content optional, but the overall match can succeed on empty input when all groups are optional - Alternative branches — Patterns with
|may match the wrong branch when backtracking across optional groups - Anchor assumptions — Without explicit
^...$anchors, partial matches on optional groups produce surprising results
Concrete example: a secrets scanner with the pattern (api.?key)?(token)? matches empty log lines in CI output. Every pipeline run that produces empty or whitespace-only log lines fires a false positive, desensitizing the team to real leaks. This exact class of bug — optional-group regex matching empty input — was the root of gitleaks#2121.
How to Detect This in Your Code
- Identify optional groups — Search your regex patterns for
(...)?or(...)*constructs - Test edge inputs — Run each pattern against empty string, single-character, and boundary inputs
- Add explicit anchors — Wrap patterns in
^(?:...)$when you need a full-string match - Use verbose mode — Add
re.VERBOSEwith comments to document group semantics
The Fix Pattern
When you hit this bug in the wild, the fix pattern is straightforward:
# Before: optional group can match empty
pattern = r'(prefix)?(suffix)?'
# After: require at least one group to match
pattern = r'(?:(prefix)(suffix)?|(suffix))'
Key Takeaways
- Always test regex against empty and minimal inputs. Optional groups make the seemingly-mandatory actually optional.
- Add unit tests for boundary cases — empty strings, whitespace-only, and partial matches
- Document group semantics — Use comments or named groups to make optionality explicit for future maintainers
- Consider a regex linter — Tools like
regexlintorpyre2can catch ambiguous optional groups before they reach production
Discovered while fixing gitleaks/gitleaks#2121. View the fix post for the specific diff.