istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/normalization_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package security
    19  
    20  import (
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"strings"
    25  	"testing"
    26  
    27  	meshconfig "istio.io/api/mesh/v1alpha1"
    28  	"istio.io/istio/pkg/test/framework"
    29  	"istio.io/istio/pkg/test/framework/components/echo"
    30  	"istio.io/istio/pkg/test/framework/components/echo/check"
    31  	"istio.io/istio/pkg/test/framework/components/echo/match"
    32  	"istio.io/istio/pkg/test/framework/components/istio"
    33  )
    34  
    35  func supportedPercentEncode(i int) bool {
    36  	special := map[int]struct{}{
    37  		0x2d: {}, // -
    38  		0x2e: {}, // .
    39  		0x2f: {}, // /
    40  		0x5c: {}, // \
    41  		0x5f: {}, // _
    42  		0x7e: {}, // ~
    43  	}
    44  	if _, found := special[i]; found {
    45  		return true
    46  	}
    47  	if 0x30 <= i && i <= 0x39 {
    48  		// 0-9
    49  		return true
    50  	}
    51  	if 0x41 <= i && i <= 0x5a {
    52  		// A-Z
    53  		return true
    54  	}
    55  	if 0x61 <= i && i <= 0x7a {
    56  		// a-z
    57  		return true
    58  	}
    59  	return false
    60  }
    61  
    62  func TestNormalization(t *testing.T) {
    63  	type expect struct {
    64  		in, out string
    65  	}
    66  	var percentEncodedCases []expect
    67  	for i := 1; i <= 0xff; i++ {
    68  		input := fmt.Sprintf("/admin%%%.2x", i)
    69  		output := input
    70  		if supportedPercentEncode(i) {
    71  			var err error
    72  			output, err = url.PathUnescape(input)
    73  			switch i {
    74  			case 0x5c:
    75  				output = strings.ReplaceAll(output, `\`, `/`)
    76  			case 0x7e:
    77  				output = strings.ReplaceAll(output, `%7e`, `~`)
    78  			}
    79  			if err != nil {
    80  				t.Errorf("failed to unescape percent encoded path %s: %v", input, err)
    81  			}
    82  		}
    83  		percentEncodedCases = append(percentEncodedCases, expect{in: input, out: output})
    84  	}
    85  	framework.NewTest(t).
    86  		Run(func(t framework.TestContext) {
    87  			cases := []struct {
    88  				name         string
    89  				ntype        meshconfig.MeshConfig_ProxyPathNormalization_NormalizationType
    90  				expectations []expect
    91  			}{
    92  				{
    93  					"None",
    94  					meshconfig.MeshConfig_ProxyPathNormalization_NONE,
    95  					[]expect{
    96  						{"/", "/"},
    97  						{"/app#foo", "/app"},
    98  						{"/app/", "/app/"},
    99  						{"/app/../admin", "/app/../admin"},
   100  						{"/app", "/app"},
   101  						{"/app//", "/app//"},
   102  						{"/app/%2f", "/app/%2f"},
   103  						{"/app%2f/", "/app%2f/"},
   104  						{"/xyz%30..//abc", "/xyz%30..//abc"},
   105  						{"/app/%2E./admin", "/app/%2E./admin"},
   106  						{`/app\admin`, `/app\admin`},
   107  						{`/app/\/\/\admin`, `/app/\/\/\admin`},
   108  						{`/%2Fapp%5cadmin%5Cabc`, `/%2Fapp%5cadmin%5Cabc`},
   109  						{`/%5Capp%2f%5c%2F%2e%2e%2fadmin%5c\abc`, `/%5Capp%2f%5c%2F%2e%2e%2fadmin%5c\abc`},
   110  						{`/app//../admin`, `/app//../admin`},
   111  						{`/app//../../admin`, `/app//../../admin`},
   112  					},
   113  				},
   114  				{
   115  					"Base",
   116  					meshconfig.MeshConfig_ProxyPathNormalization_BASE,
   117  					[]expect{
   118  						{"/", "/"},
   119  						{"/app#foo", "/app"},
   120  						{"/app/", "/app/"},
   121  						{"/app/../admin", "/admin"},
   122  						{"/app", "/app"},
   123  						{"/app//", "/app//"},
   124  						{"/app/%2f", "/app/%2f"},
   125  						{"/app%2f/", "/app%2f/"},
   126  						{"/xyz%30..//abc", "/xyz0..//abc"},
   127  						{"/app/%2E./admin", "/admin"},
   128  						{`/app\admin`, `/app/admin`},
   129  						{`/app/\/\/\admin`, `/app//////admin`},
   130  						{`/%2Fapp%5cadmin%5Cabc`, `/%2Fapp%5cadmin%5Cabc`},
   131  						{`/%5Capp%2f%5c%2F%2e%2e%2fadmin%5c\abc`, `/%5Capp%2f%5c%2F..%2fadmin%5c/abc`},
   132  						{`/app//../admin`, `/app/admin`},
   133  						{`/app//../../admin`, `/admin`},
   134  					},
   135  				},
   136  				{
   137  					"MergeSlashes",
   138  					meshconfig.MeshConfig_ProxyPathNormalization_MERGE_SLASHES,
   139  					[]expect{
   140  						{"/", "/"},
   141  						{"/app#foo", "/app"},
   142  						{"/app/", "/app/"},
   143  						{"/app/../admin", "/admin"},
   144  						{"/app", "/app"},
   145  						{"/app//", "/app/"},
   146  						{"/app/%2f", "/app/%2f"},
   147  						{"/app%2f/", "/app%2f/"},
   148  						{"/xyz%30..//abc", "/xyz0../abc"},
   149  						{"/app/%2E./admin", "/admin"},
   150  						{`/app\admin`, `/app/admin`},
   151  						{`/app/\/\/\admin`, `/app/admin`},
   152  						{`/%2Fapp%5cadmin%5Cabc`, `/%2Fapp%5cadmin%5Cabc`},
   153  						{`/%5Capp%2f%5c%2F%2e%2e%2fadmin%5c\abc`, `/%5Capp%2f%5c%2F..%2fadmin%5c/abc`},
   154  						{`/app//../admin`, `/app/admin`},
   155  						{`/app//../../admin`, `/admin`},
   156  					},
   157  				},
   158  				{
   159  					"DecodeAndMergeSlashes",
   160  					meshconfig.MeshConfig_ProxyPathNormalization_DECODE_AND_MERGE_SLASHES,
   161  					[]expect{
   162  						{"/", "/"},
   163  						{"/app#foo", "/app"},
   164  						{"/app/", "/app/"},
   165  						{"/app/../admin", "/admin"},
   166  						{"/app", "/app"},
   167  						{"/app//", "/app/"},
   168  						{"/app/%2f", "/app/"},
   169  						{"/app%2f/", "/app/"},
   170  						{"/xyz%30..//abc", "/xyz0../abc"},
   171  						{"/app/%2E./admin", "/admin"},
   172  						{`/app\admin`, `/app/admin`},
   173  						{`/app/\/\/\admin`, `/app/admin`},
   174  						{`/%2Fapp%5cadmin%5Cabc`, `/app/admin/abc`},
   175  						{`/%5Capp%2f%5c%2F%2e%2e%2fadmin%5c\abc`, `/app/admin/abc`},
   176  						{`/app//../admin`, `/app/admin`},
   177  						{`/app//../../admin`, `/admin`},
   178  						{`/%c0%2e%c0%2e/admin`, `/%c0.%c0./admin`},
   179  						{`/%c0%2fadmin`, `/%c0/admin`},
   180  					},
   181  				},
   182  				{
   183  					"NotNormalized",
   184  					meshconfig.MeshConfig_ProxyPathNormalization_DECODE_AND_MERGE_SLASHES,
   185  					[]expect{
   186  						{`/0x2e0x2e/admin`, `/0x2e0x2e/admin`},
   187  						{`/0x2e0x2e0x2fadmin`, `/0x2e0x2e0x2fadmin`},
   188  						{`/0x2e0x2e0x5cadmin`, `/0x2e0x2e0x5cadmin`},
   189  						{`/0x2f0x2fadmin`, `/0x2f0x2fadmin`},
   190  						{`/0x5c0x5cadmin`, `/0x5c0x5cadmin`},
   191  						{`/%25c0%25ae%25c1%259cadmin`, `/%25c0%25ae%25c1%259cadmin`},
   192  						{`/%25c0%25ae%25c0%25ae/admin`, `/%25c0%25ae%25c0%25ae/admin`},
   193  						{`/%25c0%25afadmin`, `/%25c0%25afadmin`},
   194  						{`/%25c1%259cadmin`, `/%25c1%259cadmin`},
   195  						{`/%252e%252e/admin`, `/%252e%252e/admin`},
   196  						{`/%252e%252e%252fadmin`, `/%252e%252e%252fadmin`},
   197  						{`/%c1%9cadmin`, `/%c1%9cadmin`},
   198  						{`/%c0%ae%c0%ae/admin`, `/%c0%ae%c0%ae/admin`},
   199  						{`/%c0%afadmin`, `/%c0%afadmin`},
   200  						{`/.../admin`, `/.../admin`},
   201  						{`/..../admin`, `/..../admin`},
   202  						{`/..;/admin`, `/..;/admin`},
   203  						{`/;/admin`, `/;/admin`},
   204  						{`/admin;a=b`, `/admin;a=b`},
   205  						{`/admin;a=b/xyz`, `/admin;a=b/xyz`},
   206  						{`/admin,a=b/xyz`, `/admin,a=b/xyz`},
   207  						{`/Admin`, `/Admin`},
   208  						{`/ADMIN`, `/ADMIN`},
   209  					},
   210  				},
   211  				{
   212  					// Test percent encode cases from %01 to %ff. (%00 is covered in the invalid group below).
   213  					name:         "PercentEncoded",
   214  					ntype:        meshconfig.MeshConfig_ProxyPathNormalization_DECODE_AND_MERGE_SLASHES,
   215  					expectations: percentEncodedCases,
   216  				},
   217  				{
   218  					"Invalid",
   219  					meshconfig.MeshConfig_ProxyPathNormalization_DECODE_AND_MERGE_SLASHES,
   220  					[]expect{
   221  						{`/admin%00`, `400`},
   222  					},
   223  				},
   224  			}
   225  			for _, tt := range cases {
   226  				t.NewSubTest(tt.name).Run(func(t framework.TestContext) {
   227  					istio.GetOrFail(t, t).PatchMeshConfigOrFail(t, t, fmt.Sprintf(`
   228  pathNormalization:
   229    normalization: %v`, tt.ntype.String()))
   230  
   231  					newTrafficTest(t, apps.Ns1.All.Instances()).
   232  						FromMatch(match.ServiceName(apps.Ns1.A.NamespacedName())).
   233  						Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   234  							for _, expected := range tt.expectations {
   235  								expected := expected
   236  								t.NewSubTest(expected.in).Run(func(t framework.TestContext) {
   237  									checker := check.URL(expected.out)
   238  									if expected.out == "400" {
   239  										checker = check.Status(http.StatusBadRequest)
   240  									}
   241  									from.CallOrFail(t, echo.CallOptions{
   242  										To:    to,
   243  										Count: 1,
   244  										HTTP: echo.HTTP{
   245  											Path: expected.in,
   246  										},
   247  										Port: echo.Port{
   248  											Name: "http",
   249  										},
   250  										Check: checker,
   251  									})
   252  								})
   253  							}
   254  						})
   255  				})
   256  			}
   257  		})
   258  }