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 }