diff options
Diffstat (limited to 'internal/redirects/validations.go')
-rw-r--r-- | internal/redirects/validations.go | 108 |
1 files changed, 92 insertions, 16 deletions
diff --git a/internal/redirects/validations.go b/internal/redirects/validations.go index 86fb0212..c469ecfc 100644 --- a/internal/redirects/validations.go +++ b/internal/redirects/validations.go @@ -18,43 +18,110 @@ var ( regexPlaceholderReplacement = regexp.MustCompile(`(?i):(?P<placeholder>[a-z]+)`) ) -// validateURL runs validations against a rule URL. +// validateFromURL validates the from URL in a redirect rule. +// It checks for various invalid cases like unsupported schemes, +// relative URLs, domain redirects without scheme, etc. // Returns `nil` if the URL is valid. -func validateURL(urlText string) error { - url, err := url.Parse(urlText) +// nolint: gocyclo +func validateFromURL(urlText string) error { + fromURL, err := url.Parse(urlText) if err != nil { return errFailedToParseURL } - // No support for domain-level redirects to outside sites: - // - `https://google.com` + // No support for domain level redirects starting with special characters without scheme: // - `//google.com` // - `/\google.com` - if url.Host != "" || url.Scheme != "" || strings.HasPrefix(url.Path, "/\\") { - return errNoDomainLevelRedirects + if (fromURL.Host == "") != (fromURL.Scheme == "") || strings.HasPrefix(fromURL.Path, "/\\") { + return errNoValidStartingInURLPath + } + + if fromURL.Scheme != "" && fromURL.Scheme != "http" && fromURL.Scheme != "https" { + return errNoValidStartingInURLPath + } + + if fromURL.Scheme == "" && fromURL.Host == "" { + // No parent traversing relative URL's with `./` or `../` + // No ambiguous URLs like bare domains `GitLab.com` + if !strings.HasPrefix(urlText, "/") { + return errNoValidStartingInURLPath + } + } + + if feature.RedirectsPlaceholders.Enabled() && strings.Count(fromURL.Path, "/*") > 1 { + return errMoreThanOneSplats } - // No parent traversing relative URL's with `./` or `../` - // No ambiguous URLs like bare domains `GitLab.com` - if !strings.HasPrefix(url.Path, "/") { - return errNoStartingForwardSlashInURLPath + return validateSplatAndPlaceholders(fromURL.Path) +} + +// validateURL runs validations against a rule URL. +// Returns `nil` if the URL is valid. +// nolint: gocyclo +func validateToURL(urlText string, status int) error { + toURL, err := url.Parse(urlText) + if err != nil { + return errFailedToParseURL } + allowedPrefix := []string{"/"} + if feature.DomainRedirects.Enabled() { + // No support for domain level redirects starting with // or special characters: + // - `//google.com` + // - `/\google.com` + if (toURL.Host == "") != (toURL.Scheme == "") || strings.HasPrefix(toURL.Path, "/\\") { + return errNoValidStartingInURLPath + } + + // No support for domain level rewrite + if isDomainURL(urlText) { + if status == http.StatusOK { + return errNoDomainLevelRewrite + } + allowedPrefix = append(allowedPrefix, "http://", "https://") + } + + // No parent traversing relative URL's with `./` or `../` + // No ambiguous URLs like bare domains `GitLab.com` + if !startsWithAnyPrefix(urlText, allowedPrefix...) { + return errNoValidStartingInURLPath + } + } else { + // No support for domain-level redirects to outside sites: + // - `https://google.com` + // - `//google.com` + // - `/\google.com` + if toURL.Host != "" || toURL.Scheme != "" || strings.HasPrefix(toURL.Path, "/\\") { + return errNoDomainLevelRedirects + } + + // No parent traversing relative URL's with `./` or `../` + // No ambiguous URLs like bare domains `GitLab.com` + if !startsWithAnyPrefix(urlText, allowedPrefix...) { + return errNoStartingForwardSlashInURLPath + } + } + + return validateSplatAndPlaceholders(toURL.Path) +} + +func validateSplatAndPlaceholders(path string) error { if feature.RedirectsPlaceholders.Enabled() { + maxPathSegments := cfg.MaxPathSegments // Limit the number of path segments a rule can contain. // This prevents the matching logic from generating regular // expressions that are too large/complex. - if strings.Count(url.Path, "/") > cfg.MaxPathSegments { + if strings.Count(path, "/") > maxPathSegments { return fmt.Errorf("url path cannot contain more than %d forward slashes", cfg.MaxPathSegments) } } else { // No support for splats, https://docs.netlify.com/routing/redirects/redirect-options/#splats - if strings.Contains(url.Path, "*") { + if strings.Contains(path, "*") { return errNoSplats } // No support for placeholders, https://docs.netlify.com/routing/redirects/redirect-options/#placeholders - if regexpPlaceholder.MatchString(url.Path) { + if regexpPlaceholder.MatchString(path) { return errNoPlaceholders } } @@ -65,11 +132,11 @@ func validateURL(urlText string) error { // validateRule runs all validation rules on the provided rule. // Returns `nil` if the rule is valid func validateRule(r netlifyRedirects.Rule) error { - if err := validateURL(r.From); err != nil { + if err := validateFromURL(r.From); err != nil { return err } - if err := validateURL(r.To); err != nil { + if err := validateToURL(r.To, r.Status); err != nil { return err } @@ -93,3 +160,12 @@ func validateRule(r netlifyRedirects.Rule) error { return nil } + +func startsWithAnyPrefix(s string, prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + return false +} |