@marcel91200 / caddy Mirror · read-only / commits / 7cc98d4d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 06:00 UTC

cmd: make splitModule strictly conform to Go module spec (#7974)

Per https://go.dev/ref/mod#go-mod-file-ident, '@' is not a valid module path character. The old implementation used strings.LastIndex to allow inputs like github.com/@user/module@v1.0.0 to parse, but such inputs are not valid module paths per the spec. Reject them explicitly. Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

Mohammed Al Sahaf committed Sep 3, 2026 at 00:43 UTC 7cc98d4d3f9842410cc6a613ac46c590a4f4bb20
2 files changed +105 -8
cmd/packagesfuncs.go
+16 -8
@@ -49,20 +49,28 @@ func cmdUpgrade(fl Flags) (int, error) {
49 func splitModule(arg string) (module, version string, err error) {
50 const versionSplit = "@"
51
52 - // accommodate module paths that have @ in them, but we can only tolerate that if there's also
53 - // a version, otherwise we don't know if it's a version separator or part of the file path
54 - lastVersionSplit := strings.LastIndex(arg, versionSplit)
55 - if lastVersionSplit < 0 {
52 + // Per https://go.dev/ref/mod#go-mod-file-ident, module paths consist of
53 + // ASCII letters, digits, and `-`, `.`, `_`, `~` only, so `@` is never a
54 + // valid path character. That makes the split unambiguous: at most one
55 + // `@` may appear, and it separates the module path from the version.
56 + switch strings.Count(arg, versionSplit) {
57 + case 0:
58 module = arg
57 - } else {
58 - module, version = arg[:lastVersionSplit], arg[lastVersionSplit+1:]
59 + case 1:
60 + idx := strings.Index(arg, versionSplit)
61 + module, version = arg[:idx], arg[idx+1:]
62 + if version == "" {
63 + return "", "", fmt.Errorf("version is required after '@'")
64 + }
65 + default:
66 + return "", "", fmt.Errorf("module path must not contain '@'")
67 }
68
69 if module == "" {
62 - err = fmt.Errorf("module name is required")
70 + return "", "", fmt.Errorf("module name is required")
71 }
72
65 - return module, version, err
73 + return module, version, nil
74 }
75
76 func cmdAddPackage(fl Flags) (int, error) {
cmd/packagesfuncs_split_test.go new
+89
@@ -0,0 +1,89 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddycmd
16 +
17 +import (
18 + "strings"
19 + "testing"
20 +)
21 +
22 +// TestSplitModule_StrictGoSpec is the regression test for splitModule
23 +// silently accepting inputs that violate go.dev/ref/mod#go-mod-file-ident
24 +// (module paths cannot contain '@'). It also checks the happy paths and
25 +// the empty-version edge case.
26 +func TestSplitModule_StrictGoSpec(t *testing.T) {
27 + for _, tt := range []struct {
28 + name string
29 + input string
30 + module string
31 + version string
32 + wantErr bool
33 + wantErrSub string
34 + }{
35 + {
36 + name: "no version",
37 + input: "github.com/caddyserver/caddy",
38 + module: "github.com/caddyserver/caddy",
39 + version: "",
40 + },
41 + {
42 + name: "with version",
43 + input: "github.com/caddyserver/caddy@v2.0.0",
44 + module: "github.com/caddyserver/caddy",
45 + version: "v2.0.0",
46 + },
47 + {
48 + name: "multiple '@' rejected",
49 + input: "github.com/@user/module@v1.0.0",
50 + wantErr: true,
51 + wantErrSub: "module path must not contain '@'",
52 + },
53 + {
54 + name: "trailing '@' with empty version rejected",
55 + input: "github.com/user/module@",
56 + wantErr: true,
57 + wantErrSub: "version is required after '@'",
58 + },
59 + {
60 + name: "empty input rejected",
61 + input: "",
62 + wantErr: true,
63 + wantErrSub: "module name is required",
64 + },
65 + } {
66 + t.Run(tt.name, func(t *testing.T) {
67 + module, version, err := splitModule(tt.input)
68 +
69 + if tt.wantErr {
70 + if err == nil {
71 + t.Fatalf("expected error containing %q, got nil", tt.wantErrSub)
72 + }
73 + if !strings.Contains(err.Error(), tt.wantErrSub) {
74 + t.Errorf("error %q does not contain %q", err.Error(), tt.wantErrSub)
75 + }
76 + return
77 + }
78 + if err != nil {
79 + t.Fatalf("unexpected error: %v", err)
80 + }
81 + if module != tt.module {
82 + t.Errorf("module: got %q, want %q", module, tt.module)
83 + }
84 + if version != tt.version {
85 + t.Errorf("version: got %q, want %q", version, tt.version)
86 + }
87 + })
88 + }
89 +}