Skip to content

Commit cac5502

Browse files
authored
Merge pull request #1054 from cappyzawa/proxy-url-validation
runtime/secrets: validate proxy URL scheme and length
2 parents c346f72 + e4755ca commit cac5502

4 files changed

Lines changed: 122 additions & 3 deletions

File tree

runtime/secrets/converter.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"errors"
2424
"fmt"
2525
"net/url"
26+
"slices"
27+
"strings"
2628

2729
"github.com/go-logr/logr"
2830
corev1 "k8s.io/api/core/v1"
@@ -132,6 +134,9 @@ func TLSConfigFromSecret(ctx context.Context, secret *corev1.Secret, opts ...TLS
132134
// The function expects the secret to contain an "address" field with the
133135
// proxy URL. Optional "username" and "password" fields can be provided
134136
// for proxy authentication.
137+
//
138+
// Supported proxy schemes are: http, https, and socks5.
139+
// The proxy URL must not exceed 2048 characters.
135140
func ProxyURLFromSecret(ctx context.Context, secret *corev1.Secret) (*url.URL, error) {
136141
addressData, exists := secret.Data[KeyAddress]
137142
if !exists {
@@ -144,10 +149,10 @@ func ProxyURLFromSecret(ctx context.Context, secret *corev1.Secret) (*url.URL, e
144149
return nil, fmt.Errorf("secret '%s': proxy address is empty", ref)
145150
}
146151

147-
proxyURL, err := url.Parse(address)
152+
proxyURL, err := parseProxyURL(address)
148153
if err != nil {
149154
ref := client.ObjectKeyFromObject(secret)
150-
return nil, fmt.Errorf("secret '%s': failed to parse proxy address '%s': %w", ref, address, err)
155+
return nil, fmt.Errorf("secret '%s': %w", ref, err)
151156
}
152157

153158
username, hasUsername := secret.Data[KeyUsername]
@@ -162,6 +167,39 @@ func ProxyURLFromSecret(ctx context.Context, secret *corev1.Secret) (*url.URL, e
162167
return proxyURL, nil
163168
}
164169

170+
func parseProxyURL(address string) (*url.URL, error) {
171+
if err := validateProxyURLString(address); err != nil {
172+
return nil, err
173+
}
174+
175+
u, err := url.Parse(address)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to parse proxy address '%s': %w", address, err)
178+
}
179+
180+
if err := validateProxyURLStruct(u); err != nil {
181+
return nil, err
182+
}
183+
184+
return u, nil
185+
}
186+
187+
func validateProxyURLString(address string) error {
188+
if len(address) > MaxProxyURLLength {
189+
return fmt.Errorf("proxy URL exceeds maximum length of %d characters", MaxProxyURLLength)
190+
}
191+
return nil
192+
}
193+
194+
func validateProxyURLStruct(u *url.URL) error {
195+
if !slices.Contains(supportedProxySchemes, u.Scheme) {
196+
supportedSchemes := strings.Join(supportedProxySchemes, ", ")
197+
return fmt.Errorf("proxy URL must use one of the supported schemes (%s), got '%s'",
198+
supportedSchemes, u.Scheme)
199+
}
200+
return nil
201+
}
202+
165203
// BasicAuthFromSecret retrieves basic authentication credentials from a Kubernetes secret.
166204
//
167205
// The function expects the secret to contain "username" and "password" fields.

runtime/secrets/converter_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"crypto/tls"
2222
"fmt"
23+
"strings"
2324
"testing"
2425

2526
"github.com/go-logr/logr"
@@ -752,6 +753,79 @@ func TestProxyURLFromSecret(t *testing.T) {
752753
),
753754
errMsg: "secret 'default/proxy-secret': failed to parse proxy address",
754755
},
756+
{
757+
name: "socks5 proxy",
758+
secret: testSecret(
759+
withName("proxy-secret"),
760+
withData(map[string][]byte{
761+
secrets.KeyAddress: []byte("socks5://socks-proxy.example.com:1080"),
762+
}),
763+
),
764+
wantURL: "socks5://socks-proxy.example.com:1080",
765+
},
766+
{
767+
name: "socks5 proxy with authentication",
768+
secret: testSecret(
769+
withName("proxy-secret"),
770+
withData(map[string][]byte{
771+
secrets.KeyAddress: []byte("socks5://socks-proxy.example.com:1080"),
772+
secrets.KeyUsername: []byte("sockuser"),
773+
secrets.KeyPassword: []byte("sockpass"),
774+
}),
775+
),
776+
wantURL: "socks5://sockuser:sockpass@socks-proxy.example.com:1080",
777+
},
778+
{
779+
name: "unsupported scheme - ftp",
780+
secret: testSecret(
781+
withName("proxy-secret"),
782+
withData(map[string][]byte{
783+
secrets.KeyAddress: []byte("ftp://ftp.example.com:21"),
784+
}),
785+
),
786+
errMsg: "proxy URL must use one of the supported schemes (http, https, socks5), got 'ftp'",
787+
},
788+
{
789+
name: "unsupported scheme - socks4",
790+
secret: testSecret(
791+
withName("proxy-secret"),
792+
withData(map[string][]byte{
793+
secrets.KeyAddress: []byte("socks4://proxy.example.com:1080"),
794+
}),
795+
),
796+
errMsg: "proxy URL must use one of the supported schemes (http, https, socks5), got 'socks4'",
797+
},
798+
{
799+
name: "URL exceeds maximum length",
800+
secret: testSecret(
801+
withName("proxy-secret"),
802+
withData(map[string][]byte{
803+
secrets.KeyAddress: []byte("http://" + strings.Repeat("a", 2050)),
804+
}),
805+
),
806+
errMsg: "proxy URL exceeds maximum length of 2048 characters",
807+
},
808+
{
809+
name: "URL at maximum length boundary",
810+
secret: testSecret(
811+
withName("proxy-secret"),
812+
withData(map[string][]byte{
813+
// Create a URL exactly 2048 characters (http:// = 7 chars, so 2041 'a's)
814+
secrets.KeyAddress: []byte("http://" + strings.Repeat("a", 2041)),
815+
}),
816+
),
817+
wantURL: "http://" + strings.Repeat("a", 2041),
818+
},
819+
{
820+
name: "missing scheme",
821+
secret: testSecret(
822+
withName("proxy-secret"),
823+
withData(map[string][]byte{
824+
secrets.KeyAddress: []byte("//proxy.example.com:8080"),
825+
}),
826+
),
827+
errMsg: "proxy URL must use one of the supported schemes (http, https, socks5), got ''",
828+
},
755829
}
756830

757831
for _, tt := range tests {

runtime/secrets/secrets.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ const (
6969
KeySSHPublicKey = "identity.pub"
7070
// KeySSHKnownHosts is the key for SSH known hosts data in secrets.
7171
KeySSHKnownHosts = "known_hosts"
72+
73+
// MaxProxyURLLength is the maximum allowed length for proxy URLs.
74+
MaxProxyURLLength = 2048
7275
)
7376

77+
// supportedProxySchemes defines the officially supported proxy URL schemes.
78+
// See https://fluxcd.io/flux/installation/configuration/proxy-setting for more information.
79+
var supportedProxySchemes = []string{"http", "https", "socks5"}
80+
7481
// AuthMethods holds all available authentication methods detected from a secret.
7582
type AuthMethods struct {
7683
Basic *BasicAuth

tests/integration/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ require (
2222
github.com/fluxcd/pkg/cache v0.12.0
2323
github.com/fluxcd/pkg/git v0.38.0
2424
github.com/fluxcd/pkg/git/gogit v0.42.0
25-
github.com/fluxcd/pkg/runtime v0.91.0
25+
github.com/fluxcd/pkg/runtime v0.92.0
2626
github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b
2727
github.com/go-git/go-git/v5 v5.16.3
2828
github.com/google/go-containerregistry v0.20.6

0 commit comments

Comments
 (0)