Skip to content

Empty string accepted as a valid version in Maven/NuGet/RubyGems, and sorts as minimum #204

Description

@logan-scott

Summary

MavenVersion, NugetVersion and RubygemsVersion accept the empty string "" as a valid version, while SemverVersion, PypiVersion and others correctly raise InvalidVersion. Worse, the accepted empty version then sorts below every real version, so downstream range logic silently produces a confident but wrong answer rather than failing.

Tested with univers 32.0.1 on CPython 3.13.

Reproducer

from univers.versions import (
    MavenVersion, NugetVersion, RubygemsVersion, SemverVersion, PypiVersion,
)

# Accepted (inconsistent):
MavenVersion("")     # -> MavenVersion(string='')
NugetVersion("")     # -> NugetVersion(string='')
RubygemsVersion("")  # -> RubygemsVersion(string='')

# Correctly rejected:
SemverVersion("")    # -> InvalidVersion
PypiVersion("")      # -> InvalidVersion

Three different behaviours across schemes, none of them an error for the first group:

MavenVersion("") < MavenVersion("0.0.1")     # True  -- sorts as minimum
RubygemsVersion("") < RubygemsVersion("0.0.1")  # True  -- sorts as minimum
NugetVersion("") < NugetVersion("0.0.1")     # TypeError:
#   '<' not supported between instances of 'NoneType' and 'Version'

So NugetVersion accepts the value at construction and then raises on comparison, which is a third distinct outcome.

Why the silent ordering is the harmful part

Consider an OSV range built from a malformed advisory:

{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": ""}]}

Because MavenVersion("") is accepted and sorts as the minimum, the interval [0, "") evaluates to the empty set. Code comparing this against the advisory's previous range concludes, with no error and no warning, that the affected set was reduced to nothing — i.e. that the range was retracted.

We hit this in a measurement study over the git history of the GitHub Advisory Database. GHSA emitted schema-invalid {"fixed": ""} into 2,107 advisory revisions during a single week in March 2023. Because three of the ecosystems we needed silently accepted it, our comparator produced roughly 1,000 spurious "affected range narrowed" events in Maven alone — a confidently wrong classification, not a crash we could catch. The npm/PyPI/Go paths were unaffected precisely because they raise InvalidVersion.

The general hazard: a caller cannot rely on "the version parsed successfully" as a validity check, and the failure is scheme-dependent, so it shows up only in some ecosystems and looks like real data.

Suggested resolution

Whatever the decision on leniency, the three schemes should agree with each other and with the rest of the library. Options, in our order of preference:

  1. Raise InvalidVersion for "" in MavenVersion, NugetVersion and RubygemsVersion, matching SemverVersion/PypiVersion. An empty string is not a version in any of these ecosystems' specs.
  2. If empty is to be accepted deliberately (see parse_version() should accept None and empty strings #10), make it consistent across all schemes and define its ordering explicitly rather than letting it fall out of the attribute tuple — the current NugetVersion TypeError on comparison shows the ordering is not well-defined today.

A test asserting identical "" behaviour across every Version subclass would prevent the schemes drifting apart again.

Related

  • parse_version() should accept None and empty strings #10 parse_version() should accept None and empty strings — related but pointing the other way; this report is about the inconsistency between schemes and the silently wrong ordering, independent of which behaviour is chosen. The comment there that "None or empty would be the same... I am not sure this would make any sense" suggests option 1 is the intended direction.
  • Do not allow creation of VersionRange with empty constraint #203 Do not allow creation of VersionRange with empty constraint — same family of problem one level up, at the range rather than the version.

Happy to send a PR for option 1 plus the cross-scheme test if that is the direction you'd like.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions