NuGet duplicate check misses versions that are equivalent after normalization
Summary
NuGet treats 3.0.0, 3.0.0.0, 3.0 and 3.0.0+meta as one version, normalized to 3.0.0. GitLab stores the normalized version of each package, but Metadatum.normalized_version_in only downcases the requested version, and the duplicate check of CreateOrUpdatePackageService and UpdatePackageFromMetadataService looks up the existing package with the version as the .nuspec writes it.
So once 3.0.0 is published, a push of 3.0.0.0, 3.0 or 3.0.0+meta finds no package: it is accepted even when duplicates are not allowed, and it creates another package for the same NuGet version. The reverse order already finds the existing package.
Steps to reproduce
- In a group, turn off Allow duplicates for NuGet packages and leave Exceptions empty.
- In a project of that group, push a package with version
3.0.0, then the same package with versions3.0.0,3.0.0.0,3.0and3.0.0+meta, a few seconds apart. - List the project's NuGet packages and download version
3.0.0.
Example Project
Not needed: the steps work in any group with the NuGet registry.
What is the current bug behavior?
The second push of 3.0.0 is refused with 409 Conflict, but the other three answer 201 Created. The project has four packages, the package index lists the same version four times, and a download of 3.0.0 returns the one pushed as 3.0.0+meta.
What is the expected correct behavior?
Every push after the first is refused with 409 Conflict, and the project has one package.
Relevant logs and/or screenshots
Pushes on a self-managed GitLab EE 17.6.1, eight seconds apart
nuget push Pkg2.Dup2 3.0.0 -> {"message":"201 Created"} 201
nuget push Pkg2.Dup2 3.0.0 -> {"message":"A package with the same name and version already exists"} 409
nuget push Pkg2.Dup2 3.0.0.0 -> {"message":"201 Created"} 201
nuget push Pkg2.Dup2 3.0 -> {"message":"201 Created"} 201
nuget push Pkg2.Dup2 3.0.0+meta -> {"message":"201 Created"} 201
--- flat container index
{"versions":["3.0","3.0.0","3.0.0+meta","3.0.0.0"]}
--- download 3.0.0 -> nuspec version inside
<version>3.0.0+meta</version>Output of checks
Reproduced on self-managed GitLab EE 17.6.1. The lookup is the same on master, where model, service and request specs show the same results.
Possible fixes
- Normalize a requested version that is a valid NuGet version in
normalized_version_in, with the algorithm already used for the stored one. - Name a file that a push adds to a package of an equivalent version after that package's version, since the download endpoint builds the file name from the version of the package it finds.
Two side effects to decide on: where duplicates are allowed (the default), a push of 3.0.0.0 after 3.0.0 becomes a new file of 3.0.0 instead of a separate package; and the metadata, download and delete endpoints share the finder, so a metadata request or DELETE for 3.0.0.0 finds 3.0.0, and a package the bug already stored as 3.0.0.0 can no longer be downloaded by that version once a newer equivalent exists. If either is unwanted, the normalized lookup can be used only to refuse a push.
I found this while working on gitlab-mcp-server, and I have a merge request with the fix and specs ready.