Skip to content

Commit d3ecc20

Browse files
authored
fix(flags): reject leading-zero semver values in local evaluation (#150)
1 parent 2c0db1d commit d3ecc20

3 files changed

Lines changed: 67 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'posthog-ruby': patch
3+
---
4+
5+
Reject semver values with leading zeros (e.g. `1.07.3`, `01.02.03`) during local feature flag evaluation, per semver 2.0.0 §2. Both override values and flag values are validated; invalid inputs raise `InconclusiveMatchError` so the condition does not match.

lib/posthog/feature_flags.rb

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,16 @@ def self.relative_date_parse_for_feature_flag_matching(value)
456456
parsed_dt
457457
end
458458

459+
# Parse a single semver numeric identifier, rejecting empty, non-digit, or
460+
# leading-zero values per semver 2.0.0 §2.
461+
def self.parse_semver_numeric(part)
462+
raise InconclusiveMatchError, 'Invalid semver format' if part.nil? || part.empty? || part !~ /^\d+$/
463+
# Semver 2.0.0 §2: numeric identifiers MUST NOT include leading zeros.
464+
raise InconclusiveMatchError, 'Invalid semver format' if part.length > 1 && part[0] == '0'
465+
466+
part.to_i
467+
end
468+
459469
# Parse a semver string into a comparable [major, minor, patch] integer array.
460470
# Handles v-prefix, whitespace, pre-release suffixes. Defaults missing components to 0.
461471
def self.parse_semver(value)
@@ -471,14 +481,9 @@ def self.parse_semver(value)
471481

472482
raise InconclusiveMatchError, 'Invalid semver format' if parts.empty? || parts[0].to_s.empty?
473483

474-
# Check for leading dot or non-numeric parts
475-
parts.each do |part|
476-
raise InconclusiveMatchError, 'Invalid semver format' if part.empty? || part !~ /^\d+$/
477-
end
478-
479-
major = parts[0].to_i
480-
minor = parts.length > 1 ? parts[1].to_i : 0
481-
patch = parts.length > 2 ? parts[2].to_i : 0
484+
major = parse_semver_numeric(parts[0])
485+
minor = parts.length > 1 ? parse_semver_numeric(parts[1]) : 0
486+
patch = parts.length > 2 ? parse_semver_numeric(parts[2]) : 0
482487

483488
[major, minor, patch]
484489
end
@@ -535,20 +540,22 @@ def self.semver_wildcard_bounds(value)
535540

536541
raise InconclusiveMatchError, 'Invalid semver wildcard format' if parts.empty?
537542

538-
parts.each do |part|
539-
raise InconclusiveMatchError, 'Invalid semver wildcard format' if part !~ /^\d+$/
543+
numeric = parts.map do |part|
544+
parse_semver_numeric(part)
545+
rescue InconclusiveMatchError
546+
raise InconclusiveMatchError, 'Invalid semver wildcard format'
540547
end
541548

542-
major = parts[0].to_i
543-
case parts.length
549+
major = numeric[0]
550+
case numeric.length
544551
when 1
545552
[[major, 0, 0], [major + 1, 0, 0]]
546553
when 2
547-
minor = parts[1].to_i
554+
minor = numeric[1]
548555
[[major, minor, 0], [major, minor + 1, 0]]
549556
else
550-
minor = parts[1].to_i
551-
patch = parts[2].to_i
557+
minor = numeric[1]
558+
patch = numeric[2]
552559
[[major, minor, patch], [major, minor, patch + 1]]
553560
end
554561
end

spec/posthog/feature_flag_spec.rb

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,10 +1888,47 @@ module PostHog
18881888
expect(FeatureFlagsPoller.match_property(property2, { 'version' => '1.2.3' })).to be true
18891889
end
18901890

1891-
it 'with semver operators and leading zeros' do
1891+
it 'with semver operators rejects override values with leading zeros' do
18921892
property = { 'key' => 'version', 'value' => '1.2.3', 'operator' => 'semver_eq' }
1893-
expect(FeatureFlagsPoller.match_property(property, { 'version' => '01.02.03' })).to be true
1894-
expect(FeatureFlagsPoller.match_property(property, { 'version' => '001.002.003' })).to be true
1893+
1894+
['01.2.3', '1.02.3', '1.2.03', '1.07.3', '001.2.3', '01.02.03', '001.002.003'].each do |bad|
1895+
expect do
1896+
FeatureFlagsPoller.match_property(property, { 'version' => bad })
1897+
end.to raise_error(InconclusiveMatchError), "expected #{bad.inspect} to be rejected"
1898+
end
1899+
end
1900+
1901+
it 'with semver operators rejects flag values with leading zeros' do
1902+
gt = { 'key' => 'version', 'value' => '1.07.3', 'operator' => 'semver_gt' }
1903+
expect do
1904+
FeatureFlagsPoller.match_property(gt, { 'version' => '2.0.0' })
1905+
end.to raise_error(InconclusiveMatchError)
1906+
1907+
caret = { 'key' => 'version', 'value' => '01.2.3', 'operator' => 'semver_caret' }
1908+
expect do
1909+
FeatureFlagsPoller.match_property(caret, { 'version' => '1.2.3' })
1910+
end.to raise_error(InconclusiveMatchError)
1911+
1912+
tilde = { 'key' => 'version', 'value' => '1.02.3', 'operator' => 'semver_tilde' }
1913+
expect do
1914+
FeatureFlagsPoller.match_property(tilde, { 'version' => '1.2.3' })
1915+
end.to raise_error(InconclusiveMatchError)
1916+
1917+
wildcard = { 'key' => 'version', 'value' => '01.*', 'operator' => 'semver_wildcard' }
1918+
expect do
1919+
FeatureFlagsPoller.match_property(wildcard, { 'version' => '1.2.3' })
1920+
end.to raise_error(InconclusiveMatchError)
1921+
end
1922+
1923+
it 'with semver operators accepts literal zero components' do
1924+
eq_zero = { 'key' => 'version', 'value' => '0.1.0', 'operator' => 'semver_eq' }
1925+
expect(FeatureFlagsPoller.match_property(eq_zero, { 'version' => '0.1.0' })).to be true
1926+
1927+
eq_zero_zero = { 'key' => 'version', 'value' => '0.0.0', 'operator' => 'semver_eq' }
1928+
expect(FeatureFlagsPoller.match_property(eq_zero_zero, { 'version' => '0.0.0' })).to be true
1929+
1930+
eq_major_zero = { 'key' => 'version', 'value' => '1.0.0', 'operator' => 'semver_eq' }
1931+
expect(FeatureFlagsPoller.match_property(eq_major_zero, { 'version' => '1.0.0' })).to be true
18951932
end
18961933

18971934
it 'with semver operators and partial versions' do

0 commit comments

Comments
 (0)