Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Bound SemVer input length
  • Loading branch information
andrew committed Sep 26, 2026
commit bbc5b26a985069398bdd49989a8fbe0d7a89f77e
21 changes: 21 additions & 0 deletions Library/Homebrew/test/vulns/semver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
.to eq [nil, nil, nil, nil]
end

it "rejects an oversized prerelease" do
expect(described_class.release_version("1.0.0-#{"a" * 251}")).to be_nil
end

it "normalises a prerelease with a prefix and build metadata" do
expect(described_class.release_version("v2026.2.22-rc.1+build.2")).to eq "2026.2.22"
end
Expand All @@ -29,6 +33,23 @@
end).to all(be_nil)
end

it "accepts versions at the input limit" do
version = "1.0.0-#{"a" * 250}"
expect(described_class.compare(version, "1.0.0")).to eq(-1)
end

it "rejects versions exceeding the input limit on either side" do
version = "1.0.0-#{"a" * 251}"
expect([described_class.compare(version, "1.0.0"), described_class.compare("1.0.0", version)])
.to all(be_nil)
end

it "rejects oversized core, prerelease, build and whitespace inputs" do
versions = ["#{"9" * 5000}.0.0", "1.0.0-#{"9" * 5000}", "1.0.0+#{"a." * 2500}a",
"#{" " * 5000}1.0.0"]
expect(versions.map { |version| described_class.compare(version, "1.0.0") }).to all(be_nil)
end

# From vers gem: basic numeric ordering
it "orders major versions numerically" do
expect(described_class.compare("1.0.0", "2.0.0")).to eq(-1)
Expand Down
14 changes: 14 additions & 0 deletions Library/Homebrew/test/vulns/vulnerability_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,14 @@ def range(type, *events)
.to all(be_nil)
end

it "leaves oversized SEMVER boundaries uncheckable" do
v = vuln("id" => "TEST-1", "affected" => [
affected("npm", "example",
range("SEMVER", { "introduced" => "0" }, { "fixed" => "1.0.0-#{"a" * 5000}" })),
])
expect(v.range_status("npm", "example", "2.0.0")).to be_nil
end

it "checks an explicit versions list when present" do
v = vuln("id" => "CVE-1", "affected" => [
affected("PyPI", "requests", versions: ["2.30.0", "2.31.0"]),
Expand Down Expand Up @@ -518,6 +526,12 @@ def range(type, *events)
expect(v.affects_version?("1.0.0")).to be true
end

it "keeps a match when the SEMVER fixed boundary exceeds the input limit" do
v = vuln({ "id" => "TEST-1" }.merge(semver_range({ "introduced" => "0" },
{ "fixed" => "1.0.0-#{"a" * 5000}" })))
expect(v.affects_version?("2.0.0")).to be true
end

it "matches an open-ended range introduced at v0" do
v = vuln({ "id" => "CVE-2024-1234" }.merge(semver_range({ "introduced" => "v0" })))
expect(v.affects_version?("1.2.3")).to be true
Expand Down
8 changes: 7 additions & 1 deletion Library/Homebrew/vulns/semver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ module Homebrew
module Vulns
# SemVer 2.0 comparison for OSV `SEMVER` ranges (https://semver.org/#spec-item-11).
# Kept separate from `::Version`, whose ordering differs for prerelease and
# build metadata. Minor/patch may be omitted; other spec violations return `nil`.
# build metadata. Minor/patch may be omitted; other spec violations or
# inputs over 256 bytes return `nil`.
module Semver
MAX_LENGTH = 256
private_constant :MAX_LENGTH

CORE_SEGMENT = "(0|[1-9]\\d*)"
private_constant :CORE_SEGMENT

Expand Down Expand Up @@ -52,6 +56,8 @@ def self.release_version(version)

sig { params(version: String).returns(T.nilable({ core: [Integer, Integer, Integer], prerelease: T::Array[String] })) }
private_class_method def self.parse(version)
return if version.bytesize > MAX_LENGTH

match = version.strip.sub(/\Av/i, "").match(SEMVER_REGEX)
return if match.nil?

Expand Down
Loading