Duplicate package exception regex with alternation matches only a prefix or suffix

Summary

The duplicate package exception regex is meant to match the whole package name or version. Namespace::PackageSetting.duplicates_allowed? and the Terraform module CreatePackageService#duplicates_not_allowed? wrap the group's setting as "\\A#{regex}\\z", but an alternation binds looser than the anchors: 9\.9\.9|8\.8\.8 becomes \A9\.9\.9|8\.8\.8\z, which matches any value that starts with 9.9.9 or ends with 8.8.8.

This affects the four formats with the setting (Maven, generic, NuGet and Terraform modules):

  • With duplicates not allowed, a duplicate such as 9.9.9-rc is accepted.
  • With duplicates allowed, where the regex names what must not be duplicated, the same values are refused.

Steps to reproduce

  1. In a group, turn off Allow duplicates for generic packages and set Exceptions to 9\.9\.9|8\.8\.8.

  2. In a project of that group, upload the same file twice as version 9.9.9-rc:

    curl --request PUT --upload-file f.txt --header "PRIVATE-TOKEN: <your_access_token>" \
      --url "https://gitlab.example.com/api/v4/projects/<project_id>/packages/generic/dup/9.9.9-rc/f.txt"
  3. Repeat with version 7.8.8.8, and with the exception set to 9\.9\.9 alone, each time under a new package name.

Example Project

Not needed: any group with generic packages.

What is the current bug behavior?

With 9\.9\.9|8\.8\.8, both uploads of 9.9.9-rc succeed, and so do both uploads of 7.8.8.8. With 9\.9\.9 alone, the second upload of 9.9.9-rc is refused.

What is the expected correct behavior?

Each alternative has to match the whole name or version, as a regex without | does, so the second upload of each is refused with 400 Bad Request.

Relevant logs and/or screenshots

Self-managed GitLab EE 17.6.1, one package name per run
== regex '9\\.9\\.9' (single alternative), version 9.9.9-rc
{"data":{"updateNamespacePackageSettings":{"packageSettings":{"genericDuplicatesAllowed":false,"genericDuplicateExceptionRegex":"9\\.9\\.9"},"errors":[]}}}
PUT dupA/9.9.9-rc/f.txt -> 201
PUT dupA/9.9.9-rc/f.txt -> 400
== regex '9\\.9\\.9|8\\.8\\.8' (alternation), version 9.9.9-rc
{"data":{"updateNamespacePackageSettings":{"packageSettings":{"genericDuplicatesAllowed":false,"genericDuplicateExceptionRegex":"9\\.9\\.9|8\\.8\\.8"},"errors":[]}}}
PUT dupB/9.9.9-rc/f.txt -> 201
PUT dupB/9.9.9-rc/f.txt -> 201
== same regex, version 7.8.8.8
PUT dupC/7.8.8.8/f.txt -> 201
PUT dupC/7.8.8.8/f.txt -> 201

Output of checks

Reproduced on self-managed GitLab EE 17.6.1 with duplicates not allowed; that release builds the regex as master does. On master, model and service specs show the same results for all four formats, with duplicates allowed or not.

Possible fixes

Match the setting with RE2's full match instead of adding anchors to its text, through a full_match? on Gitlab::UntrustedRegexp that calls RE2::Regexp#full_match?. Wrapping it as \A(?:...)\z is not enough: RE2 reads an unclosed \Q as quoting the rest of the pattern, so a valid setting such as \Q1.0.0-beta would no longer compile. I have a merge request ready.

Edited by 🤖 GitLab Bot 🤖