diff --git a/test/e2e/network/funny_ips.go b/test/e2e/network/funny_ips.go new file mode 100644 index 00000000000..4b4d35a694b --- /dev/null +++ b/test/e2e/network/funny_ips.go @@ -0,0 +1,201 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package network + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/onsi/ginkgo" + + "k8s.io/kubernetes/test/e2e/framework" + e2epod "k8s.io/kubernetes/test/e2e/framework/pod" + e2eservice "k8s.io/kubernetes/test/e2e/framework/service" + e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" + "k8s.io/kubernetes/test/e2e/network/common" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + clientset "k8s.io/client-go/kubernetes" + netutils "k8s.io/utils/net" +) + +// What are funny IPs: +// The adjective is because of the curl blog that explains the history and the problem of liberal +// parsing of IP addresses and the consequences and security risks caused the lack of normalization, +// mainly due to the use of different notations to abuse parsers misalignment to bypass filters. +// xref: https://daniel.haxx.se/blog/2021/04/19/curl-those-funny-ipv4-addresses/ +// +// Since golang 1.17, IPv4 addresses with leading zeros are rejected by the standard library. +// xref: https://github.com/golang/go/issues/30999 +// +// Because this change on the parsers can cause that previous valid data become invalid, Kubernetes +// forked the old parsers allowing leading zeros on IPv4 address to not break the compatibility. +// +// Kubernetes interprets leading zeros on IPv4 addresses as decimal, users must not rely on parser +// alignment to not being impacted by the associated security advisory: CVE-2021-29923 golang +// standard library "net" - Improper Input Validation of octal literals in golang 1.16.2 and below +// standard library "net" results in indeterminate SSRF & RFI vulnerabilities. xref: +// https://nvd.nist.gov/vuln/detail/CVE-2021-29923 +// +// Kubernetes +// ip := net.ParseIPSloppy("192.168.0.011") +// ip.String() yields 192.168.0.11 +// +// BSD stacks +// ping 192.168.0.011 +// PING 192.168.0.011 (192.168.0.9) 56(84) bytes of data. + +var _ = common.SIGDescribe("CVE-2021-29923", func() { + var ( + ns string + cs clientset.Interface + ) + + f := framework.NewDefaultFramework("funny-ips") + + ginkgo.BeforeEach(func() { + if framework.TestContext.ClusterIsIPv6() { + e2eskipper.Skipf("The test doesn't apply to IPv6 addresses, only IPv4 addresses are affected by CVE-2021-29923") + } + ns = f.Namespace.Name + cs = f.ClientSet + }) + + /* + Create a ClusterIP service with a ClusterIP with leading zeros and check that it is reachable in the IP interpreted as decimal. + If the Service is not reachable only FAIL if it is reachable in the IP interpreted as binary, because it will be exposed to CVE-2021-29923. + IMPORTANT: CoreDNS since version 1.8.5 discard IPs with leading zeros so Services are not resolvable, and is probably that + most of the ecosystem has done the same, however, Kubernetes doesn't impose any restriction, users should migrate their IPs. + */ + ginkgo.It("IPv4 Service Type ClusterIP with leading zeros should work interpreted as decimal", func() { + serviceName := "funny-ip" + // Use a very uncommon port to reduce the risk of conflicts with other tests that create services. + servicePort := 7180 + jig := e2eservice.NewTestJig(cs, ns, serviceName) + + clusterIPZero, clusterIPOctal := getServiceIPWithLeadingZeros(cs) + if clusterIPZero == "" { + e2eskipper.Skipf("Couldn't find a free ClusterIP") + } + + ginkgo.By("creating service " + serviceName + " with type=ClusterIP and ip " + clusterIPZero + " in namespace " + ns) + _, err := jig.CreateTCPService(func(svc *v1.Service) { + svc.Spec.ClusterIP = clusterIPZero // IP with a leading zero + svc.Spec.Type = v1.ServiceTypeClusterIP + svc.Spec.Ports = []v1.ServicePort{ + {Port: int32(servicePort), Name: "http", Protocol: v1.ProtocolTCP, TargetPort: intstr.FromInt(9376)}, + } + }) + framework.ExpectNoError(err) + + err = jig.CreateServicePods(1) + framework.ExpectNoError(err) + + execPod := e2epod.CreateExecPodOrFail(cs, ns, "execpod", nil) + ip := netutils.ParseIPSloppy(clusterIPZero) + cmd := fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", ip.String(), servicePort) + err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) { + stdout, err := framework.RunHostCmd(execPod.Namespace, execPod.Name, cmd) + if err != nil { + framework.Logf("Service reachability failing with error: %v\nRetrying...", err) + return false, nil + } + trimmed := strings.TrimSpace(stdout) + if trimmed != "" { + return true, nil + } + return false, nil + }) + // Service is working on the expected IP. + if err == nil { + return + } + // It may happen that the component implementing Services discard the IP. + // We have to check that the Service is not reachable in the address interpreted as decimal. + cmd = fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", clusterIPOctal, servicePort) + err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) { + stdout, err := framework.RunHostCmd(execPod.Namespace, execPod.Name, cmd) + if err != nil { + framework.Logf("Service reachability failing with error: %v\nRetrying...", err) + return false, nil + } + trimmed := strings.TrimSpace(stdout) + if trimmed != "" { + return true, nil + } + return false, nil + }) + // Ouch, Service has worked on IP interpreted as octal. + if err == nil { + framework.Failf("WARNING: Your Cluster interprets Service ClusterIP %s as %s, please see https://nvd.nist.gov/vuln/detail/CVE-2021-29923", clusterIPZero, clusterIPOctal) + } + framework.Logf("Service reachability failing for Service against ClusterIP %s and %s, most probably leading zeros on IPs are not supported by the cluster proxy", clusterIPZero, clusterIPOctal) + }) + +}) + +// Try to get a free IP that has different decimal and binary interpretation with leading zeros. +// Return both IPs, the one interpretad as binary and the one interpreted as decimal. +// Return empty if not IPs are found. +func getServiceIPWithLeadingZeros(cs clientset.Interface) (string, string) { + clusterIPMap := map[string]struct{}{} + var clusterIPPrefix string + // Dump all the IPs and look for the ones we want. + list, err := cs.CoreV1().Services(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{}) + framework.ExpectNoError(err) + for _, svc := range list.Items { + if len(svc.Spec.ClusterIP) == 0 || svc.Spec.ClusterIP == v1.ClusterIPNone { + continue + } + // Get the clusterIP prefix, needed later for building the IPs with octal format. + if clusterIPPrefix == "" { + // split the IP by "." + // get the first 3 values + // join them again using dots, dropping the last field + clusterIPPrefix = strings.Join(strings.Split(svc.Spec.ClusterIP, ".")[:3], ".") + } + // Canonicalize IP. + ip := netutils.ParseIPSloppy(svc.Spec.ClusterIP) + clusterIPMap[ip.String()] = struct{}{} + } + + // Try to get a free IP between x.x.x.11 and x.x.x.31, this assumes a /27 Service IP range that should be safe. + for i := 11; i < 31; i++ { + ip := fmt.Sprintf("%s.%d", clusterIPPrefix, i) + // Check the IP in decimal format is free. + if _, ok := clusterIPMap[ip]; !ok { + o, err := strconv.ParseInt(fmt.Sprintf("%d", i), 8, 64) + // Omit ips without binary representation i.e. x.x.x.018, x.x.x.019 ... + if err != nil { + continue + } + ipOctal := fmt.Sprintf("%s.%d", clusterIPPrefix, o) + // Check the same IP in octal format is free. + if _, ok := clusterIPMap[ipOctal]; !ok { + // Add a leading zero to the decimal format. + return fmt.Sprintf("%s.0%d", clusterIPPrefix, i), ipOctal + } + } + } + return "", "" +}