From 5e3bf6a8b2a58a0b5e35c994b59c63cbc240ca6e Mon Sep 17 00:00:00 2001 From: Allan Roger Reid Date: Wed, 14 Oct 2026 09:05:00 +0000 Subject: [PATCH 1/5] Fix env.LookupEnv treating values that start with "env" as references LookupEnv sent every value that began with the letters "env" to the remote fetch, so a plain value such as "envroot" resolved to empty. In MinIO AIStor, MINIO_ROOT_USER=envroot stopped the server, and two such root values made it use the default credentials (miniohq/aistor#8200). A value is now a reference only when its scheme, in any letter case, is "env" or "env+tls" followed by "://". --- env/env_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ env/web_env.go | 17 +++++----- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/env/env_test.go b/env/env_test.go index 6d5300b3..2114f3ea 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -18,10 +18,13 @@ package env import ( + "crypto/x509" "fmt" "net/http" "net/http/httptest" "net/url" + "os" + "strings" "testing" "time" @@ -165,3 +168,87 @@ func TestGetEnv(t *testing.T) { t.Fatalf("Expected 'value-new', but got %s", v) } } + +func TestLookupEnvReferenceScheme(t *testing.T) { + ts := startTestServer(t) + tlsTS := httptest.NewTLSServer(ts.Config.Handler) + t.Cleanup(tlsTS.Close) + pool := x509.NewCertPool() + pool.AddCert(tlsTS.Certificate()) + prevCAs := globalRootCAs + RegisterGlobalCAs(pool) + t.Cleanup(func() { RegisterGlobalCAs(prevCAs) }) + + host := func(s *httptest.Server) string { + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + return u.Host + } + const path = "/webhook/v1/getenv/default/minio" + const fetched = "http://127.0.0.{1..4}:9000/data{1...4}" + + testCases := []struct { + name, value, cached string + want, wantUser, wantPass string + wantErr bool + }{ + {name: "plain envroot", value: "envroot", want: "envroot"}, + {name: "plain value with stale cache", value: "envroot", cached: "stale", want: "envroot"}, + {name: "plain env", value: "env", want: "env"}, + {name: "plain environment", value: "environment", want: "environment"}, + {name: "plain env+tlsx", value: "env+tlsx", want: "env+tlsx"}, + {name: "opaque env colon", value: "env:foo", want: "env:foo"}, + {name: "single slash", value: "env:/x", want: "env:/x"}, + {name: "other scheme envs", value: "envs://h/x", want: "envs://h/x"}, + {name: "other scheme env-tls", value: "env-tls://h/x", want: "env-tls://h/x"}, + {name: "other scheme env+tlsx", value: "env+tlsx://h/x", want: "env+tlsx://h/x"}, + {name: "leading space", value: " env://x", want: " env://x"}, + {name: "unicode fold of tls", value: "ENV+TL\u017f://x", want: "ENV+TL\u017f://x"}, + {name: "env reference", value: "env://minio:minio123@" + host(ts) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, + {name: "upper case env reference", value: "ENV://minio:minio123@" + host(ts) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, + {name: "env+tls reference", value: "env+tls://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, + {name: "mixed case env+tls reference", value: "env+TLS://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, + {name: "unparsable reference", value: "env://minio:minio123@127.0.0.1:1/%zz", wantErr: true}, + {name: "unreachable reference uses cache", value: "env://minio:minio123@127.0.0.1:1" + path, cached: "cached", want: "cached"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("MINIO_ARGS", tc.value) + t.Setenv("_MINIO_ARGS", tc.cached) + if tc.cached == "" { + if err := os.Unsetenv("_MINIO_ARGS"); err != nil { + t.Fatal(err) + } + } + got, user, pass, err := LookupEnv("MINIO_ARGS") + if (err != nil) != tc.wantErr { + t.Fatalf("got error %v, want error %v", err, tc.wantErr) + } + if got != tc.want || user != tc.wantUser || pass != tc.wantPass { + t.Fatalf("got (%q, %q, %q), want (%q, %q, %q)", got, user, pass, tc.want, tc.wantUser, tc.wantPass) + } + cache, cached := os.LookupEnv("_MINIO_ARGS") + switch { + case tc.wantUser != "": + if cache != tc.want { + t.Fatalf("cache holds %q, want %q", cache, tc.want) + } + case tc.cached == "": + if cached { + t.Fatalf("cache set to %q, want unset", cache) + } + case cache != tc.cached: + t.Fatalf("cache holds %q, want %q", cache, tc.cached) + } + want := strings.TrimSpace(tc.want) + if tc.wantErr { + want = "default" + } + if v := Get("MINIO_ARGS", "default"); v != want { + t.Fatalf("Get returned %q, want %q", v, want) + } + }) + } +} diff --git a/env/web_env.go b/env/web_env.go index a9dac11d..043c5363 100644 --- a/env/web_env.go +++ b/env/web_env.go @@ -167,19 +167,20 @@ func Environ() []string { // LookupEnv retrieves the value of the environment variable named by `key`. // -// If the value of the variable starts with "env://" or "env+tls://" it is -// fetched from the referenced remote server. The fetched value is cached in a -// separate environment variable prefixed with an underscore for subsequent -// lookups should the remote server be unreachable. When fetching from a remote -// server the username and password used are also returned. +// If the value of the variable starts with "env://" or "env+tls://", in any +// letter case, it is fetched from the referenced remote server. The fetched +// value is cached in a separate environment variable prefixed with an +// underscore for subsequent lookups should the remote server be unreachable. +// When fetching from a remote server the username and password used are also +// returned. // // For regular environment variables the value is returned as-is with empty // credentials. func LookupEnv(key string) (string, string, string, error) { v, ok := os.LookupEnv(key) - if ok && strings.HasPrefix(v, webEnvScheme) { - // If env value starts with `env*://` - // continue to parse and fetch from remote + if scheme, _, found := strings.Cut(v, "://"); ok && found && (strings.ToLower(scheme) == webEnvScheme || strings.ToLower(scheme) == webEnvSchemeSecure) { + // Not strings.EqualFold: it also folds U+017F to "s", and + // url.Parse rejects such a scheme. var err error v, user, pwd, err := getEnvValueFromHTTP(strings.TrimSpace(v), key) if err != nil { From ad23581aea66b0ff62195dd82b5b9834256f132a Mon Sep 17 00:00:00 2001 From: Allan Roger Reid Date: Wed, 14 Oct 2026 14:28:12 +0000 Subject: [PATCH 2/5] test: make the unparsable reference row fail only on a parse error The row pointed at a closed port, so a dial error also satisfied it, and a change that stopped rejecting unparsable references went unnoticed. It now points at the live test server. --- env/env_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env/env_test.go b/env/env_test.go index 2114f3ea..c6221c3b 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -210,7 +210,7 @@ func TestLookupEnvReferenceScheme(t *testing.T) { {name: "upper case env reference", value: "ENV://minio:minio123@" + host(ts) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, {name: "env+tls reference", value: "env+tls://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, {name: "mixed case env+tls reference", value: "env+TLS://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, - {name: "unparsable reference", value: "env://minio:minio123@127.0.0.1:1/%zz", wantErr: true}, + {name: "unparsable reference", value: "env://minio:minio123@" + host(ts) + "/%zz", wantErr: true}, {name: "unreachable reference uses cache", value: "env://minio:minio123@127.0.0.1:1" + path, cached: "cached", want: "cached"}, } for _, tc := range testCases { From 20e96b1538d495cddd83035ac9c9e9afce127998 Mon Sep 17 00:00:00 2001 From: Allan Roger Reid Date: Wed, 14 Oct 2026 16:48:28 +0000 Subject: [PATCH 3/5] fix(deps): bump golang.org/x/net to v0.60.0 govulncheck fails on main since the Go vulnerability database update of 2026-10-08. golang.org/x/net v0.58.0 has four symbol-level findings (GO-2026-6603, GO-2026-6611, GO-2026-6612, GO-2026-6617) and one package-level finding (GO-2026-6610); v0.60.0 fixes all five. The other golang.org/x versions are the ones x/net v0.60.0 and x/text v0.42.0 require. --- go.mod | 14 +++++++------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 0dc1bfe5..f4f3ed3a 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,9 @@ require ( github.com/zeebo/xxh3 v1.1.0 go.etcd.io/etcd/client/v3 v3.6.12 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.56.0 + golang.org/x/crypto v0.57.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sys v0.47.0 + golang.org/x/sys v0.48.0 ) require ( @@ -229,9 +229,9 @@ require ( go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/tools v0.49.0 // indirect gopkg.in/ini.v1 v1.67.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -264,8 +264,8 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.12 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/text v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/grpc v1.83.2 // indirect diff --git a/go.sum b/go.sum index c026689f..08bf65dd 100644 --- a/go.sum +++ b/go.sum @@ -596,8 +596,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -610,8 +610,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -621,8 +621,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -632,8 +632,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -651,21 +651,21 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= @@ -677,8 +677,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= From ead6fc6d2723be26649dfd9574fd2651dbe452b1 Mon Sep 17 00:00:00 2001 From: Allan Roger Reid Date: Wed, 14 Oct 2026 18:57:21 +0000 Subject: [PATCH 4/5] test: require the unparsable reference row to open no connection The row passed when a change sent the malformed reference and then failed on the response, because any error satisfied it. The row now points at its own listener and requires no accepted connection and a url.EscapeError. --- env/env_test.go | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/env/env_test.go b/env/env_test.go index c6221c3b..71c8216b 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -19,12 +19,15 @@ package env import ( "crypto/x509" + "errors" "fmt" + "net" "net/http" "net/http/httptest" "net/url" "os" "strings" + "sync/atomic" "testing" "time" @@ -178,6 +181,22 @@ func TestLookupEnvReferenceScheme(t *testing.T) { prevCAs := globalRootCAs RegisterGlobalCAs(pool) t.Cleanup(func() { RegisterGlobalCAs(prevCAs) }) + unparsableLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { unparsableLn.Close() }) + var unparsableConns atomic.Int32 + go func() { + for { + c, err := unparsableLn.Accept() + if err != nil { + return + } + unparsableConns.Add(1) + c.Close() + } + }() host := func(s *httptest.Server) string { u, err := url.Parse(s.URL) @@ -192,7 +211,7 @@ func TestLookupEnvReferenceScheme(t *testing.T) { testCases := []struct { name, value, cached string want, wantUser, wantPass string - wantErr bool + wantParseErr bool }{ {name: "plain envroot", value: "envroot", want: "envroot"}, {name: "plain value with stale cache", value: "envroot", cached: "stale", want: "envroot"}, @@ -210,7 +229,7 @@ func TestLookupEnvReferenceScheme(t *testing.T) { {name: "upper case env reference", value: "ENV://minio:minio123@" + host(ts) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, {name: "env+tls reference", value: "env+tls://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, {name: "mixed case env+tls reference", value: "env+TLS://minio:minio123@" + host(tlsTS) + path, want: fetched, wantUser: "minio", wantPass: "minio123"}, - {name: "unparsable reference", value: "env://minio:minio123@" + host(ts) + "/%zz", wantErr: true}, + {name: "unparsable reference", value: "env://minio:minio123@" + unparsableLn.Addr().String() + "/%zz", wantParseErr: true}, {name: "unreachable reference uses cache", value: "env://minio:minio123@127.0.0.1:1" + path, cached: "cached", want: "cached"}, } for _, tc := range testCases { @@ -223,8 +242,12 @@ func TestLookupEnvReferenceScheme(t *testing.T) { } } got, user, pass, err := LookupEnv("MINIO_ARGS") - if (err != nil) != tc.wantErr { - t.Fatalf("got error %v, want error %v", err, tc.wantErr) + if (err != nil) != tc.wantParseErr { + t.Fatalf("got error %v, want error %v", err, tc.wantParseErr) + } + var escapeErr url.EscapeError + if tc.wantParseErr && !errors.As(err, &escapeErr) { + t.Fatalf("got error %v, want a url.Parse escape error", err) } if got != tc.want || user != tc.wantUser || pass != tc.wantPass { t.Fatalf("got (%q, %q, %q), want (%q, %q, %q)", got, user, pass, tc.want, tc.wantUser, tc.wantPass) @@ -243,12 +266,15 @@ func TestLookupEnvReferenceScheme(t *testing.T) { t.Fatalf("cache holds %q, want %q", cache, tc.cached) } want := strings.TrimSpace(tc.want) - if tc.wantErr { + if tc.wantParseErr { want = "default" } if v := Get("MINIO_ARGS", "default"); v != want { t.Fatalf("Get returned %q, want %q", v, want) } + if n := unparsableConns.Load(); tc.wantParseErr && n != 0 { + t.Fatalf("unparsable reference opened %d connection(s), want 0", n) + } }) } } From 99ac9919d0dc8a50bd3b4efe1e68324f69ae4f36 Mon Sep 17 00:00:00 2001 From: Allan Roger Reid Date: Fri, 16 Oct 2026 17:36:40 +0000 Subject: [PATCH 5/5] refactor: lowercase the env reference scheme once The ok check is redundant: an unset variable reads as "", which has no "://", so found already rejects it. Lowercase the scheme only when the value contains "://", so a plain value such as a password is never copied. --- env/web_env.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/env/web_env.go b/env/web_env.go index 043c5363..a09eef62 100644 --- a/env/web_env.go +++ b/env/web_env.go @@ -177,8 +177,12 @@ func Environ() []string { // For regular environment variables the value is returned as-is with empty // credentials. func LookupEnv(key string) (string, string, string, error) { - v, ok := os.LookupEnv(key) - if scheme, _, found := strings.Cut(v, "://"); ok && found && (strings.ToLower(scheme) == webEnvScheme || strings.ToLower(scheme) == webEnvSchemeSecure) { + v := os.Getenv(key) + scheme, _, found := strings.Cut(v, "://") + if found { + scheme = strings.ToLower(scheme) + } + if found && (scheme == webEnvScheme || scheme == webEnvSchemeSecure) { // Not strings.EqualFold: it also folds U+017F to "s", and // url.Parse rejects such a scheme. var err error