Skip to content

Commit d6330fa

Browse files
committed
update path validation
1 parent d78f57b commit d6330fa

File tree

3 files changed

+234
-3
lines changed

3 files changed

+234
-3
lines changed

internal/k8s/validation.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,15 @@ const (
7272
const (
7373
commaDelimiter = ","
7474
annotationValueFmt = `([^"$\\]|\\[^$])*`
75-
pathFmt = `/[^\s{};\\]*`
7675
jwtTokenValueFmt = "\\$" + annotationValueFmt
7776
)
7877

7978
const (
8079
annotationValueFmtErrMsg = `a valid annotation value must have all '"' escaped and must not contain any '$' or end with an unescaped '\'`
81-
pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`"
8280
jwtTokenValueFmtErrMsg = `a valid annotation value must start with '$', have all '"' escaped, and must not contain any '$' or end with an unescaped '\'`
8381
)
8482

8583
var (
86-
pathRegexp = regexp.MustCompile("^" + pathFmt + "$")
8784
validAnnotationValueRegex = regexp.MustCompile("^" + annotationValueFmt + "$")
8885
validJWTTokenAnnotationValueRegex = regexp.MustCompile("^" + jwtTokenValueFmt + "$")
8986
)
@@ -875,6 +872,13 @@ func validateBackend(backend *networking.IngressBackend, fieldPath *field.Path)
875872
return allErrs
876873
}
877874

875+
const (
876+
pathFmt = `/[^\s;\\]*`
877+
pathErrMsg = "must start with / and must not include any whitespace character or `;`"
878+
)
879+
880+
var pathRegexp = regexp.MustCompile("^" + pathFmt + "$")
881+
878882
func validatePath(path string, fieldPath *field.Path) field.ErrorList {
879883
allErrs := field.ErrorList{}
880884

@@ -887,6 +891,78 @@ func validatePath(path string, fieldPath *field.Path) field.ErrorList {
887891
return append(allErrs, field.Invalid(fieldPath, path, msg))
888892
}
889893

894+
allErrs = append(allErrs, validateRegexPath(path, fieldPath)...)
895+
allErrs = append(allErrs, validateCurlyBraces(path, fieldPath)...)
896+
allErrs = append(allErrs, validateIllegalKeywords(path, fieldPath)...)
897+
898+
return allErrs
899+
}
900+
901+
func validateRegexPath(path string, fieldPath *field.Path) field.ErrorList {
902+
allErrs := field.ErrorList{}
903+
904+
if _, err := regexp.Compile(path); err != nil {
905+
return append(allErrs, field.Invalid(fieldPath, path, fmt.Sprintf("must be a valid regular expression: %v", err)))
906+
}
907+
908+
if err := ValidateEscapedString(path, "*.jpg", "^/images/image_*.png$"); err != nil {
909+
return append(allErrs, field.Invalid(fieldPath, path, err.Error()))
910+
}
911+
912+
return allErrs
913+
}
914+
915+
const (
916+
curlyBracesFmt = `\{(.*?)\}`
917+
alphabetFmt = `[A-Za-z]`
918+
curlyBracesMsg = `must not include curly braces containing alphabetical characters`
919+
)
920+
921+
var curlyBracesFmtRegexp = regexp.MustCompile(curlyBracesFmt) //nolint:gofumpt
922+
var alphabetFmtRegex = regexp.MustCompile(alphabetFmt)
923+
924+
func validateCurlyBraces(path string, fieldPath *field.Path) field.ErrorList {
925+
allErrs := field.ErrorList{}
926+
927+
bracesContents := curlyBracesFmtRegexp.FindAllStringSubmatch(path, -1)
928+
for _, v := range bracesContents {
929+
if alphabetFmtRegex.MatchString(v[1]) {
930+
return append(allErrs, field.Invalid(fieldPath, path, curlyBracesMsg))
931+
}
932+
}
933+
return allErrs
934+
}
935+
936+
const (
937+
escapedStringsFmt = `([^"\\]|\\.)*`
938+
escapedStringsErrMsg = `must have all '"' (double quotes) escaped and must not end with an unescaped '\' (backslash)`
939+
)
940+
941+
var escapedStringsFmtRegexp = regexp.MustCompile("^" + escapedStringsFmt + "$")
942+
943+
// ValidateEscapedString validates an escaped string.
944+
func ValidateEscapedString(body string, examples ...string) error {
945+
if !escapedStringsFmtRegexp.MatchString(body) {
946+
msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, examples...)
947+
return fmt.Errorf(msg)
948+
}
949+
return nil
950+
}
951+
952+
const (
953+
illegalKeywordFmt = `/etc|/root`
954+
illegalKeywordErrMsg = `must not contain invalid paths`
955+
)
956+
957+
var illegalKeywordFmtRegexp = regexp.MustCompile("^" + illegalKeywordFmt + "$")
958+
959+
func validateIllegalKeywords(path string, fieldPath *field.Path) field.ErrorList {
960+
allErrs := field.ErrorList{}
961+
962+
if illegalKeywordFmtRegexp.MatchString(path) {
963+
return append(allErrs, field.Invalid(fieldPath, path, illegalKeywordErrMsg))
964+
}
965+
890966
return allErrs
891967
}
892968

internal/k8s/validation_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3335,3 +3335,152 @@ func TestGetSpecServices(t *testing.T) {
33353335
}
33363336
}
33373337
}
3338+
3339+
func TestValidateRegexPath(t *testing.T) {
3340+
t.Parallel()
3341+
tests := []struct {
3342+
regexPath string
3343+
msg string
3344+
}{
3345+
{
3346+
regexPath: "~ ^/foo.*\\.jpg",
3347+
msg: "case sensitive regexp",
3348+
},
3349+
{
3350+
regexPath: "~* ^/Bar.*\\.jpg",
3351+
msg: "case insensitive regexp",
3352+
},
3353+
{
3354+
regexPath: `~ ^/f\"oo.*\\.jpg`,
3355+
msg: "regexp with escaped double quotes",
3356+
},
3357+
{
3358+
regexPath: "~ [0-9a-z]{4}[0-9]+",
3359+
msg: "regexp with brackets",
3360+
},
3361+
}
3362+
3363+
for _, test := range tests {
3364+
allErrs := validateRegexPath(test.regexPath, field.NewPath("path"))
3365+
if len(allErrs) != 0 {
3366+
t.Errorf("validateRegexPath(%v) returned errors for valid input for the case of %v", test.regexPath, test.msg)
3367+
}
3368+
}
3369+
}
3370+
3371+
func TestValidateRegexPathFails(t *testing.T) {
3372+
t.Parallel()
3373+
tests := []struct {
3374+
regexPath string
3375+
msg string
3376+
}{
3377+
{
3378+
regexPath: "~ [{",
3379+
msg: "invalid regexp",
3380+
},
3381+
{
3382+
regexPath: `~ /foo"`,
3383+
msg: "unescaped double quotes",
3384+
},
3385+
{
3386+
regexPath: `~"`,
3387+
msg: "empty regex",
3388+
},
3389+
{
3390+
regexPath: `~ /foo\`,
3391+
msg: "ending in backslash",
3392+
},
3393+
}
3394+
3395+
for _, test := range tests {
3396+
allErrs := validateRegexPath(test.regexPath, field.NewPath("path"))
3397+
if len(allErrs) == 0 {
3398+
t.Errorf("validateRegexPath(%v) returned no errors for invalid input for the case of %v", test.regexPath, test.msg)
3399+
}
3400+
}
3401+
}
3402+
3403+
func TestValidatePath(t *testing.T) {
3404+
t.Parallel()
3405+
3406+
validPaths := []string{
3407+
"/",
3408+
"/path",
3409+
"/a-1/_A/",
3410+
"/[A-Za-z]{6}/[a-z]{1,2}",
3411+
"/[0-9a-z]{4}[0-9]",
3412+
}
3413+
3414+
for _, path := range validPaths {
3415+
allErrs := validatePath(path, field.NewPath("path"))
3416+
if len(allErrs) > 0 {
3417+
t.Errorf("validatePath(%q) returned errors %v for valid input", path, allErrs)
3418+
}
3419+
}
3420+
3421+
invalidPaths := []string{
3422+
"",
3423+
" /",
3424+
"/ ",
3425+
"/abc;",
3426+
`/path\`,
3427+
`/path\n`,
3428+
`/etc/nginx/secrets`,
3429+
}
3430+
3431+
for _, path := range invalidPaths {
3432+
allErrs := validatePath(path, field.NewPath("path"))
3433+
if len(allErrs) == 0 {
3434+
t.Errorf("validatePath(%q) returned no errors for invalid input", path)
3435+
}
3436+
}
3437+
}
3438+
3439+
func TestValidateCurlyBraces(t *testing.T) {
3440+
t.Parallel()
3441+
3442+
validPaths := []string{
3443+
"/[a-z]{1,2}",
3444+
"/[A-Z]{6}",
3445+
"/[A-Z]{6}/[a-z]{1,2}",
3446+
"/path",
3447+
"/abc}{abc",
3448+
}
3449+
3450+
for _, path := range validPaths {
3451+
allErrs := validateCurlyBraces(path, field.NewPath("path"))
3452+
if len(allErrs) > 0 {
3453+
t.Errorf("validatePath(%q) returned errors %v for valid input", path, allErrs)
3454+
}
3455+
}
3456+
3457+
invalidPaths := []string{
3458+
"/[A-Z]{a}",
3459+
"/{abc}abc",
3460+
"/abc{a1}",
3461+
}
3462+
3463+
for _, path := range invalidPaths {
3464+
allErrs := validateCurlyBraces(path, field.NewPath("path"))
3465+
if len(allErrs) == 0 {
3466+
t.Errorf("validateCurlyBraces(%q) returned no errors for invalid input", path)
3467+
}
3468+
}
3469+
}
3470+
3471+
func TestValidateIllegalKeywords(t *testing.T) {
3472+
t.Parallel()
3473+
3474+
invalidPaths := []string{
3475+
"/root",
3476+
"/etc/nginx/secrets",
3477+
"/etc/passwd",
3478+
}
3479+
3480+
for _, path := range invalidPaths {
3481+
allErrs := validateIllegalKeywords(path, field.NewPath("path"))
3482+
if len(allErrs) == 0 {
3483+
t.Errorf("validateCurlyBraces(%q) returned no errors for invalid input", path)
3484+
}
3485+
}
3486+
}

pkg/apis/configuration/validation/virtualserver_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,10 @@ func TestValidateRegexPath(t *testing.T) {
14651465
regexPath: `~ ^/f\"oo.*\\.jpg`,
14661466
msg: "regexp with escaped double quotes",
14671467
},
1468+
{
1469+
regexPath: "~ [0-9a-z]{4}[0-9]+",
1470+
msg: "regexp with brackets",
1471+
},
14681472
}
14691473

14701474
for _, test := range tests {
@@ -1526,6 +1530,8 @@ func TestValidateRoutePath(t *testing.T) {
15261530
invalidPaths := []string{
15271531
"",
15281532
"invalid",
1533+
// regex without preceding "~*" modifier
1534+
"^/foo.*\\.jpg",
15291535
}
15301536

15311537
for _, path := range invalidPaths {

0 commit comments

Comments
 (0)