Skip to content

Commit bfe4f73

Browse files
committed
fix: replace regex with string parsing to resolve ReDoS warning
Replace SEMVER_EXTRACT_RE regex with simple string splitting to eliminate nested quantifiers that CodeQL flagged as polynomial-redos.
1 parent f891a71 commit bfe4f73

2 files changed

Lines changed: 15 additions & 27 deletions

File tree

posthog/feature_flags.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -763,30 +763,25 @@ def relative_date_parse_for_feature_flag_matching(
763763
return None
764764

765765

766-
SEMVER_EXTRACT_RE = re.compile(r"(\d+(?:\.\d+)+)")
767-
768-
769766
def parse_semver(value: str) -> tuple:
770767
"""Parse a semver string into a comparable (major, minor, patch) integer tuple.
771768
772769
Matches the behavior of the sortableSemver HogQL function:
773-
- Uses regex to extract version numbers from strings like "v1.2.3-alpha"
774770
- Handles v-prefix, whitespace, pre-release suffixes
775771
- Defaults missing components to 0 (e.g., 1.2 -> 1.2.0)
776772
Raises ValueError if parsing fails.
777773
"""
778-
text = str(value)
779-
if len(text) > 200:
780-
raise ValueError("Version string too long")
781-
match = SEMVER_EXTRACT_RE.search(text)
782-
if not match:
783-
raise ValueError("Invalid semver format")
774+
text = str(value).strip().lstrip("vV")
775+
# Strip pre-release/build metadata suffix
776+
text = text.split("-")[0].split("+")[0]
777+
parts = text.split(".")
784778

785-
parts = match.group(1).split(".")
779+
if not parts or not parts[0]:
780+
raise ValueError("Invalid semver format")
786781

787782
major = int(parts[0])
788-
minor = int(parts[1]) if len(parts) > 1 else 0
789-
patch = int(parts[2]) if len(parts) > 2 else 0
783+
minor = int(parts[1]) if len(parts) > 1 and parts[1] else 0
784+
patch = int(parts[2]) if len(parts) > 2 and parts[2] else 0
790785

791786
return (major, minor, patch)
792787

@@ -821,21 +816,13 @@ def _wildcard_bounds(value: str) -> tuple:
821816
1.* means >=1.0.0 <2.0.0
822817
1.2.* means >=1.2.0 <1.3.0
823818
"""
824-
# Strip wildcards and trailing dots, then extract version digits
825-
cleaned = str(value).replace("*", "").rstrip(".")
819+
cleaned = str(value).strip().lstrip("vV").replace("*", "").rstrip(".")
826820
if not cleaned:
827821
raise ValueError("Invalid wildcard pattern")
828822

829-
match = SEMVER_EXTRACT_RE.search(cleaned)
830-
if match:
831-
parts = match.group(1).split(".")
832-
else:
833-
# Try single number (e.g., "1" from "1.*")
834-
cleaned = cleaned.strip()
835-
if cleaned.isdigit():
836-
parts = [cleaned]
837-
else:
838-
raise ValueError("Invalid wildcard pattern")
823+
parts = [p for p in cleaned.split(".") if p]
824+
if not parts:
825+
raise ValueError("Invalid wildcard pattern")
839826

840827
if len(parts) == 1:
841828
major = int(parts[0])

posthog/test/test_feature_flags.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4362,8 +4362,9 @@ def test_match_properties_semver_edge_cases(self):
43624362
with self.assertRaises(InconclusiveMatchError):
43634363
match_property(prop, {"version": ""})
43644364

4365-
# Leading dot: ".1.2.3" -> regex extracts "1.2.3"
4366-
self.assertTrue(match_property(prop, {"version": ".1.2.3"}))
4365+
# Leading dot: ".1.2.3" -> invalid, empty first component
4366+
with self.assertRaises(InconclusiveMatchError):
4367+
match_property(prop, {"version": ".1.2.3"})
43674368

43684369
# Caret with v-prefix in flag value
43694370
prop_caret_v = self.property(

0 commit comments

Comments
 (0)