191
splitPath: []string{".php"},
192
wantPos: 9,
193
},
194
+ // Regression tests adapted from FrankenPHP advisories
195
+ // GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase
196
+ // matched Unicode equivalents of ASCII letters as ".php", and an
197
+ // inner non-ASCII byte path could leave the match flag stale.
198
+ {
199
+ name: "non-ascii byte after dot must not match",
200
+ path: "/PoC-match-unset.¡.txt",
201
+ splitPath: []string{".php"},
202
+ wantPos: -1,
203
+ },
204
+ {
205
+ name: "non-ascii byte mid-extension must not match",
206
+ path: "/script.p\xc2\xa1p",
207
+ splitPath: []string{".php"},
208
+ wantPos: -1,
209
+ },
210
+ {
211
+ name: "small full stop ﹒ in extension must not match",
212
+ path: "/shell﹒php",
213
+ splitPath: []string{".php"},
214
+ wantPos: -1,
215
+ },
216
+ {
217
+ name: "fullwidth full stop . in extension must not match",
218
+ path: "/shell.php",
219
+ splitPath: []string{".php"},
220
+ wantPos: -1,
221
+ },
222
+ {
223
+ name: "fullwidth p in extension must not match",
224
+ path: "/shell.php",
225
+ splitPath: []string{".php"},
226
+ wantPos: -1,
227
+ },
228
+ {
229
+ name: "circled php must not match",
230
+ path: "/shell.ⓟⓗⓟ",
231
+ splitPath: []string{".php"},
232
+ wantPos: -1,
233
+ },
234
+ {
235
+ name: "mathematical sans-serif bold php must not match",
236
+ path: "/shell.\U0001D5FD\U0001D5F5\U0001D5FD",
237
+ splitPath: []string{".php"},
238
+ wantPos: -1,
239
+ },
240
+ {
241
+ name: "mathematical script php must not match",
242
+ path: "/shell.\U0001D4C5\U0001D4BD\U0001D4C5",
243
+ splitPath: []string{".php"},
244
+ wantPos: -1,
245
+ },
246
+ {
247
+ name: "circled php with later real php still picks the real one",
248
+ path: "/shell.ⓟⓗⓟ.anything-after-payload.php",
249
+ splitPath: []string{".php"},
250
+ // "/shell." (7) + "ⓟⓗⓟ" (3*3 bytes) + ".anything-after-payload.php" (27) = 43
251
+ wantPos: 43,
252
+ },
253
}
254
255
for _, tt := range tests {
303
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
304
}
305
}
306
+
307
+// TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP
308
+// advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII
309
+// byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase
310
+// folding fullwidth/mathematical/circled letters onto ASCII). Every payload
311
+// below produced a false positive in the vulnerable implementation; none
312
+// must match here.
313
+func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) {
314
+ t.Parallel()
315
+
316
+ tr := Transport{SplitPath: []string{".php"}}
317
+ payloads := []string{
318
+ "/PoC-match-unset.¡.txt", // GHSA-3g8v: stale match=true on IndexString fallback
319
+ "/shell﹒php", // U+FE52 small full stop
320
+ "/shell.php", // U+FF0E fullwidth full stop
321
+ "/shell.php", // U+FF50 fullwidth p
322
+ "/shell.php", // U+FF48 fullwidth h
323
+ "/shell.php", // U+FF50 fullwidth p (trailing)
324
+ "/shell.\U0001D5C1\U0001D5B5\U0001D5C1", // mathematical sans-serif p/h
325
+ "/shell.\U0001D5FD\U0001D5F5\U0001D5FD", // mathematical sans-serif bold p/h
326
+ "/shell.\U0001D4C5\U0001D4BD\U0001D4C5", // mathematical script p/h
327
+ "/shell.ⓟⓗⓟ", // circled latin small
328
+ }
329
+
330
+ for _, p := range payloads {
331
+ assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p)
332
+ }
333
+}