Skip to content

Commit 3406737

Browse files
haywoodshcoolbry95
authored andcommitted
Optimise path validation (nginx#3094)
* update path validation
1 parent 88fb869 commit 3406737

File tree

3 files changed

+250
-3
lines changed

3 files changed

+250
-3
lines changed

internal/k8s/validation.go

Lines changed: 81 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,80 @@ 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 (
922+
curlyBracesFmtRegexp = regexp.MustCompile(curlyBracesFmt)
923+
alphabetFmtRegexp = regexp.MustCompile(alphabetFmt)
924+
)
925+
926+
func validateCurlyBraces(path string, fieldPath *field.Path) field.ErrorList {
927+
allErrs := field.ErrorList{}
928+
929+
bracesContents := curlyBracesFmtRegexp.FindAllStringSubmatch(path, -1)
930+
for _, v := range bracesContents {
931+
if alphabetFmtRegexp.MatchString(v[1]) {
932+
return append(allErrs, field.Invalid(fieldPath, path, curlyBracesMsg))
933+
}
934+
}
935+
return allErrs
936+
}
937+
938+
const (
939+
escapedStringsFmt = `([^"\\]|\\.)*`
940+
escapedStringsErrMsg = `must have all '"' (double quotes) escaped and must not end with an unescaped '\' (backslash)`
941+
)
942+
943+
var escapedStringsFmtRegexp = regexp.MustCompile("^" + escapedStringsFmt + "$")
944+
945+
// ValidateEscapedString validates an escaped string.
946+
func ValidateEscapedString(body string, examples ...string) error {
947+
if !escapedStringsFmtRegexp.MatchString(body) {
948+
msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, examples...)
949+
return fmt.Errorf(msg)
950+
}
951+
return nil
952+
}
953+
954+
const (
955+
illegalKeywordFmt = `/etc/|/root|/var|\\n|\\r`
956+
illegalKeywordErrMsg = `must not contain invalid paths`
957+
)
958+
959+
var illegalKeywordFmtRegexp = regexp.MustCompile("^" + illegalKeywordFmt + "$")
960+
961+
func validateIllegalKeywords(path string, fieldPath *field.Path) field.ErrorList {
962+
allErrs := field.ErrorList{}
963+
964+
if illegalKeywordFmtRegexp.MatchString(path) {
965+
return append(allErrs, field.Invalid(fieldPath, path, illegalKeywordErrMsg))
966+
}
967+
890968
return allErrs
891969
}
892970

internal/k8s/validation_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3335,3 +3335,166 @@ 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 curly braces",
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+
"/foo.*\\.jpg",
3413+
"/Bar.*\\.jpg",
3414+
`/f\"oo.*\\.jpg`,
3415+
"/[0-9a-z]{4}[0-9]+",
3416+
"/[a-z]{1,2}",
3417+
"/[A-Z]{6}",
3418+
"/[A-Z]{6}/[a-z]{1,2}",
3419+
"/path",
3420+
"/abc}{abc",
3421+
}
3422+
3423+
for _, path := range validPaths {
3424+
allErrs := validatePath(path, field.NewPath("path"))
3425+
if len(allErrs) > 0 {
3426+
t.Errorf("validatePath(%q) returned errors %v for valid input", path, allErrs)
3427+
}
3428+
}
3429+
3430+
invalidPaths := []string{
3431+
"",
3432+
" /",
3433+
"/ ",
3434+
"/abc;",
3435+
`/path\`,
3436+
`/path\n`,
3437+
`/var/run/secrets`,
3438+
"/{autoindex on; root /var/run/secrets;}location /tea",
3439+
"/{root}",
3440+
}
3441+
3442+
for _, path := range invalidPaths {
3443+
allErrs := validatePath(path, field.NewPath("path"))
3444+
if len(allErrs) == 0 {
3445+
t.Errorf("validatePath(%q) returned no errors for invalid input", path)
3446+
}
3447+
}
3448+
}
3449+
3450+
func TestValidateCurlyBraces(t *testing.T) {
3451+
t.Parallel()
3452+
3453+
validPaths := []string{
3454+
"/[a-z]{1,2}",
3455+
"/[A-Z]{6}",
3456+
"/[A-Z]{6}/[a-z]{1,2}",
3457+
"/path",
3458+
"/abc}{abc",
3459+
}
3460+
3461+
for _, path := range validPaths {
3462+
allErrs := validateCurlyBraces(path, field.NewPath("path"))
3463+
if len(allErrs) > 0 {
3464+
t.Errorf("validatePath(%q) returned errors %v for valid input", path, allErrs)
3465+
}
3466+
}
3467+
3468+
invalidPaths := []string{
3469+
"/[A-Z]{a}",
3470+
"/{abc}abc",
3471+
"/abc{a1}",
3472+
}
3473+
3474+
for _, path := range invalidPaths {
3475+
allErrs := validateCurlyBraces(path, field.NewPath("path"))
3476+
if len(allErrs) == 0 {
3477+
t.Errorf("validateCurlyBraces(%q) returned no errors for invalid input", path)
3478+
}
3479+
}
3480+
}
3481+
3482+
func TestValidateIllegalKeywords(t *testing.T) {
3483+
t.Parallel()
3484+
3485+
invalidPaths := []string{
3486+
"/root",
3487+
"/etc/nginx/secrets",
3488+
"/etc/passwd",
3489+
"/var/run/secrets",
3490+
`\n`,
3491+
`\r`,
3492+
}
3493+
3494+
for _, path := range invalidPaths {
3495+
allErrs := validateIllegalKeywords(path, field.NewPath("path"))
3496+
if len(allErrs) == 0 {
3497+
t.Errorf("validateCurlyBraces(%q) returned no errors for invalid input", path)
3498+
}
3499+
}
3500+
}

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 curly braces",
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)