fix: reject invalid Zotfile versions
- commit
- 0adeca6789cff850e37cc57b6130cc684a487d8b
- author
- patriceckhart · · 2026-08-10 05:48 UTC
- parent
- 13b118a
- signature
- unsigned
3 files · +25 -5
M packages/agent/update.go +12 -4
| @@ -122,14 +122,22 @@ func buildInfo(current, latest, url string) UpdateInfo { | |||
| 122 | 122 | } | |
| 123 | 123 | ||
| 124 | 124 | // versionLess reports whether a is an older semantic version than b. | |
| 125 | - | // Invalid versions fail closed so the updater never offers a downgrade. | |
| 125 | + | // Invalid versions return false so update checks never offer a downgrade. | |
| 126 | 126 | func versionLess(a, b string) bool { | |
| 127 | + | comparison, err := compareVersions(a, b) | |
| 128 | + | return err == nil && comparison < 0 | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func compareVersions(a, b string) (int, error) { | |
| 127 | 132 | a = normalizedVersion(a) | |
| 128 | 133 | b = normalizedVersion(b) | |
| 129 | - | if !semver.IsValid(a) || !semver.IsValid(b) { | |
| 130 | - | return false | |
| 134 | + | if !semver.IsValid(a) { | |
| 135 | + | return 0, fmt.Errorf("invalid semantic version %q", strings.TrimPrefix(a, "v")) | |
| 136 | + | } | |
| 137 | + | if !semver.IsValid(b) { | |
| 138 | + | return 0, fmt.Errorf("invalid semantic version %q", strings.TrimPrefix(b, "v")) | |
| 131 | 139 | } | |
| 132 | - | return semver.Compare(a, b) < 0 | |
| 140 | + | return semver.Compare(a, b), nil | |
| 133 | 141 | } | |
| 134 | 142 | ||
| 135 | 143 | func normalizedVersion(v string) string { | |
| @@ -122,14 +122,22 @@ func buildInfo(current, latest, url string) UpdateInfo { | |||||
| 122 | } | 122 | } | ||
| 123 | 123 | ||||
| 124 | // versionLess reports whether a is an older semantic version than b. | 124 | // versionLess reports whether a is an older semantic version than b. | ||
| 125 | - | // Invalid versions fail closed so the updater never offers a downgrade. | 125 | + | // Invalid versions return false so update checks never offer a downgrade. |
| 126 | func versionLess(a, b string) bool { | 126 | func versionLess(a, b string) bool { | ||
| 127 | + | comparison, err := compareVersions(a, b) | |||
| 128 | + | return err == nil && comparison < 0 | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func compareVersions(a, b string) (int, error) { | |||
| 127 | a = normalizedVersion(a) | 132 | a = normalizedVersion(a) | ||
| 128 | b = normalizedVersion(b) | 133 | b = normalizedVersion(b) | ||
| 129 | - | if !semver.IsValid(a) || !semver.IsValid(b) { | 134 | + | if !semver.IsValid(a) { |
| 130 | - | return false | 135 | + | return 0, fmt.Errorf("invalid semantic version %q", strings.TrimPrefix(a, "v")) |
| 136 | + | } | |||
| 137 | + | if !semver.IsValid(b) { | |||
| 138 | + | return 0, fmt.Errorf("invalid semantic version %q", strings.TrimPrefix(b, "v")) | |||
| 131 | } | 139 | } | ||
| 132 | - | return semver.Compare(a, b) < 0 | 140 | + | return semver.Compare(a, b), nil |
| 133 | } | 141 | } | ||
| 134 | 142 | ||||
| 135 | func normalizedVersion(v string) string { | 143 | func normalizedVersion(v string) string { | ||
M packages/agent/zotfile.go +5 -1
| @@ -752,7 +752,11 @@ func checkZotfileRequirements(zf zotfileLoaded, version string) error { | |||
| 752 | 752 | if versionOnly(version) == "0.0.0" { | |
| 753 | 753 | return fmt.Errorf("agent requires zot %s or newer; unversioned development builds cannot satisfy min_zot", min) | |
| 754 | 754 | } | |
| 755 | - | if versionLess(version, min) { | |
| 755 | + | comparison, err := compareVersions(version, min) | |
| 756 | + | if err != nil { | |
| 757 | + | return fmt.Errorf("validate min_zot requirement: %w", err) | |
| 758 | + | } | |
| 759 | + | if comparison < 0 { | |
| 756 | 760 | return fmt.Errorf("agent requires zot %s or newer; running %s", min, versionOnly(version)) | |
| 757 | 761 | } | |
| 758 | 762 | } | |
| @@ -752,7 +752,11 @@ func checkZotfileRequirements(zf zotfileLoaded, version string) error { | |||||
| 752 | if versionOnly(version) == "0.0.0" { | 752 | if versionOnly(version) == "0.0.0" { | ||
| 753 | return fmt.Errorf("agent requires zot %s or newer; unversioned development builds cannot satisfy min_zot", min) | 753 | return fmt.Errorf("agent requires zot %s or newer; unversioned development builds cannot satisfy min_zot", min) | ||
| 754 | } | 754 | } | ||
| 755 | - | if versionLess(version, min) { | 755 | + | comparison, err := compareVersions(version, min) |
| 756 | + | if err != nil { | |||
| 757 | + | return fmt.Errorf("validate min_zot requirement: %w", err) | |||
| 758 | + | } | |||
| 759 | + | if comparison < 0 { | |||
| 756 | return fmt.Errorf("agent requires zot %s or newer; running %s", min, versionOnly(version)) | 760 | return fmt.Errorf("agent requires zot %s or newer; running %s", min, versionOnly(version)) | ||
| 757 | } | 761 | } | ||
| 758 | } | 762 | } | ||
M packages/agent/zotfile_test.go +8 -0
| @@ -223,6 +223,14 @@ func TestCheckZotfileMinVersion(t *testing.T) { | |||
| 223 | 223 | if err := checkZotfileRequirements(zf, "0.3.0"); err != nil { | |
| 224 | 224 | t.Fatalf("minimum version rejected: %v", err) | |
| 225 | 225 | } | |
| 226 | + | if err := checkZotfileRequirements(zf, "not-a-version"); err == nil { | |
| 227 | + | t.Fatal("invalid zot version accepted") | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | zf.Manifest.Runtime.MinZot = "not-a-version" | |
| 231 | + | if err := checkZotfileRequirements(zf, "0.3.0"); err == nil { | |
| 232 | + | t.Fatal("invalid min_zot requirement accepted") | |
| 233 | + | } | |
| 226 | 234 | } | |
| 227 | 235 | ||
| 228 | 236 | func TestApplyZotfileModelRequirementsRejectsUnsupportedFields(t *testing.T) { | |
| @@ -223,6 +223,14 @@ func TestCheckZotfileMinVersion(t *testing.T) { | |||||
| 223 | if err := checkZotfileRequirements(zf, "0.3.0"); err != nil { | 223 | if err := checkZotfileRequirements(zf, "0.3.0"); err != nil { | ||
| 224 | t.Fatalf("minimum version rejected: %v", err) | 224 | t.Fatalf("minimum version rejected: %v", err) | ||
| 225 | } | 225 | } | ||
| 226 | + | if err := checkZotfileRequirements(zf, "not-a-version"); err == nil { | |||
| 227 | + | t.Fatal("invalid zot version accepted") | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | zf.Manifest.Runtime.MinZot = "not-a-version" | |||
| 231 | + | if err := checkZotfileRequirements(zf, "0.3.0"); err == nil { | |||
| 232 | + | t.Fatal("invalid min_zot requirement accepted") | |||
| 233 | + | } | |||
| 226 | } | 234 | } | ||
| 227 | 235 | ||||
| 228 | func TestApplyZotfileModelRequirementsRejectsUnsupportedFields(t *testing.T) { | 236 | func TestApplyZotfileModelRequirementsRejectsUnsupportedFields(t *testing.T) { | ||