k8s.io/kubernetes@v1.29.3/test/e2e/network/funny_ips.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package network
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/onsi/ginkgo/v2"
    27  
    28  	"k8s.io/kubernetes/test/e2e/framework"
    29  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    30  	e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
    31  	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
    32  	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
    33  	"k8s.io/kubernetes/test/e2e/network/common"
    34  
    35  	v1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/util/intstr"
    38  	"k8s.io/apimachinery/pkg/util/wait"
    39  	clientset "k8s.io/client-go/kubernetes"
    40  	admissionapi "k8s.io/pod-security-admission/api"
    41  	netutils "k8s.io/utils/net"
    42  )
    43  
    44  // What are funny IPs:
    45  // The adjective is because of the curl blog that explains the history and the problem of liberal
    46  // parsing of IP addresses and the consequences and security risks caused the lack of normalization,
    47  // mainly due to the use of different notations to abuse parsers misalignment to bypass filters.
    48  // xref: https://daniel.haxx.se/blog/2021/04/19/curl-those-funny-ipv4-addresses/
    49  //
    50  // Since golang 1.17, IPv4 addresses with leading zeros are rejected by the standard library.
    51  // xref: https://github.com/golang/go/issues/30999
    52  //
    53  // Because this change on the parsers can cause that previous valid data become invalid, Kubernetes
    54  // forked the old parsers allowing leading zeros on IPv4 address to not break the compatibility.
    55  //
    56  // Kubernetes interprets leading zeros on IPv4 addresses as decimal, users must not rely on parser
    57  // alignment to not being impacted by the associated security advisory: CVE-2021-29923 golang
    58  // standard library "net" - Improper Input Validation of octal literals in golang 1.16.2 and below
    59  // standard library "net" results in indeterminate SSRF & RFI vulnerabilities. xref:
    60  // https://nvd.nist.gov/vuln/detail/CVE-2021-29923
    61  //
    62  // Kubernetes
    63  // ip := net.ParseIPSloppy("192.168.0.011")
    64  // ip.String() yields 192.168.0.11
    65  //
    66  // BSD stacks
    67  // ping 192.168.0.011
    68  // PING 192.168.0.011 (192.168.0.9) 56(84) bytes of data.
    69  
    70  var _ = common.SIGDescribe("CVE-2021-29923", func() {
    71  	var (
    72  		ns string
    73  		cs clientset.Interface
    74  	)
    75  
    76  	f := framework.NewDefaultFramework("funny-ips")
    77  	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
    78  
    79  	ginkgo.BeforeEach(func() {
    80  		if framework.TestContext.ClusterIsIPv6() {
    81  			e2eskipper.Skipf("The test doesn't apply to IPv6 addresses, only IPv4 addresses are affected by CVE-2021-29923")
    82  		}
    83  		ns = f.Namespace.Name
    84  		cs = f.ClientSet
    85  	})
    86  
    87  	/*
    88  		Create a ClusterIP service with a ClusterIP with leading zeros and check that it is reachable in the IP interpreted as decimal.
    89  		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.
    90  		IMPORTANT: CoreDNS since version 1.8.5 discard IPs with leading zeros so Services are not resolvable, and is probably that
    91  		most of the ecosystem has done the same, however, Kubernetes doesn't impose any restriction, users should migrate their IPs.
    92  	*/
    93  	ginkgo.It("IPv4 Service Type ClusterIP with leading zeros should work interpreted as decimal", func(ctx context.Context) {
    94  		serviceName := "funny-ip"
    95  		// Use a very uncommon port to reduce the risk of conflicts with other tests that create services.
    96  		servicePort := 7180
    97  		jig := e2eservice.NewTestJig(cs, ns, serviceName)
    98  
    99  		clusterIPZero, clusterIPOctal := getServiceIPWithLeadingZeros(ctx, cs)
   100  		if clusterIPZero == "" {
   101  			e2eskipper.Skipf("Couldn't find a free ClusterIP")
   102  		}
   103  
   104  		ginkgo.By("creating service " + serviceName + " with type=ClusterIP and ip " + clusterIPZero + " in namespace " + ns)
   105  		_, err := jig.CreateTCPService(ctx, func(svc *v1.Service) {
   106  			svc.Spec.ClusterIP = clusterIPZero // IP with a leading zero
   107  			svc.Spec.Type = v1.ServiceTypeClusterIP
   108  			svc.Spec.Ports = []v1.ServicePort{
   109  				{Port: int32(servicePort), Name: "http", Protocol: v1.ProtocolTCP, TargetPort: intstr.FromInt32(9376)},
   110  			}
   111  		})
   112  		framework.ExpectNoError(err)
   113  
   114  		err = jig.CreateServicePods(ctx, 1)
   115  		framework.ExpectNoError(err)
   116  
   117  		execPod := e2epod.CreateExecPodOrFail(ctx, cs, ns, "execpod", nil)
   118  		ip := netutils.ParseIPSloppy(clusterIPZero)
   119  		cmd := fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", ip.String(), servicePort)
   120  		err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) {
   121  			stdout, err := e2eoutput.RunHostCmd(execPod.Namespace, execPod.Name, cmd)
   122  			if err != nil {
   123  				framework.Logf("Service reachability failing with error: %v\nRetrying...", err)
   124  				return false, nil
   125  			}
   126  			trimmed := strings.TrimSpace(stdout)
   127  			if trimmed != "" {
   128  				return true, nil
   129  			}
   130  			return false, nil
   131  		})
   132  		// Service is working on the expected IP.
   133  		if err == nil {
   134  			return
   135  		}
   136  		// It may happen that the component implementing Services discard the IP.
   137  		// We have to check that the Service is not reachable in the address interpreted as decimal.
   138  		cmd = fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", clusterIPOctal, servicePort)
   139  		err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) {
   140  			stdout, err := e2eoutput.RunHostCmd(execPod.Namespace, execPod.Name, cmd)
   141  			if err != nil {
   142  				framework.Logf("Service reachability failing with error: %v\nRetrying...", err)
   143  				return false, nil
   144  			}
   145  			trimmed := strings.TrimSpace(stdout)
   146  			if trimmed != "" {
   147  				return true, nil
   148  			}
   149  			return false, nil
   150  		})
   151  		// Ouch, Service has worked on IP interpreted as octal.
   152  		if err == nil {
   153  			framework.Failf("WARNING: Your Cluster interprets Service ClusterIP %s as %s, please see https://nvd.nist.gov/vuln/detail/CVE-2021-29923", clusterIPZero, clusterIPOctal)
   154  		}
   155  		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)
   156  	})
   157  
   158  })
   159  
   160  // Try to get a free IP that has different decimal and binary interpretation with leading zeros.
   161  // Return both IPs, the one interpretad as binary and the one interpreted as decimal.
   162  // Return empty if not IPs are found.
   163  func getServiceIPWithLeadingZeros(ctx context.Context, cs clientset.Interface) (string, string) {
   164  	clusterIPMap := map[string]struct{}{}
   165  	var clusterIPPrefix string
   166  	// Dump all the IPs and look for the ones we want.
   167  	list, err := cs.CoreV1().Services(metav1.NamespaceAll).List(ctx, metav1.ListOptions{})
   168  	framework.ExpectNoError(err)
   169  	for _, svc := range list.Items {
   170  		if len(svc.Spec.ClusterIP) == 0 || svc.Spec.ClusterIP == v1.ClusterIPNone {
   171  			continue
   172  		}
   173  		// Get the clusterIP prefix, needed later for building the IPs with octal format.
   174  		if clusterIPPrefix == "" {
   175  			// split the IP by "."
   176  			// get the first 3 values
   177  			// join them again using dots, dropping the last field
   178  			clusterIPPrefix = strings.Join(strings.Split(svc.Spec.ClusterIP, ".")[:3], ".")
   179  		}
   180  		// Canonicalize IP.
   181  		ip := netutils.ParseIPSloppy(svc.Spec.ClusterIP)
   182  		clusterIPMap[ip.String()] = struct{}{}
   183  	}
   184  
   185  	// 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.
   186  	for i := 11; i < 31; i++ {
   187  		ip := fmt.Sprintf("%s.%d", clusterIPPrefix, i)
   188  		// Check the IP in decimal format is free.
   189  		if _, ok := clusterIPMap[ip]; !ok {
   190  			o, err := strconv.ParseInt(fmt.Sprintf("%d", i), 8, 64)
   191  			// Omit ips without binary representation i.e. x.x.x.018, x.x.x.019 ...
   192  			if err != nil {
   193  				continue
   194  			}
   195  			ipOctal := fmt.Sprintf("%s.%d", clusterIPPrefix, o)
   196  			// Check the same IP in octal format is free.
   197  			if _, ok := clusterIPMap[ipOctal]; !ok {
   198  				// Add a leading zero to the decimal format.
   199  				return fmt.Sprintf("%s.0%d", clusterIPPrefix, i), ipOctal
   200  			}
   201  		}
   202  	}
   203  	return "", ""
   204  }