mirror of
https://github.com/nais/wonderwall.git
synced 2026-02-14 17:49:54 +00:00
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package url
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
mw "github.com/nais/wonderwall/pkg/middleware"
|
|
)
|
|
|
|
// Used to check final redirects are not susceptible to open redirects.
|
|
// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
|
|
var invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
|
|
|
|
var _ Validator = &AbsoluteValidator{}
|
|
|
|
type Validator interface {
|
|
IsValidRedirect(r *http.Request, redirect string) bool
|
|
}
|
|
|
|
type AbsoluteValidator struct {
|
|
allowedDomains []string
|
|
}
|
|
|
|
func NewAbsoluteValidator(allowedDomains []string) *AbsoluteValidator {
|
|
return &AbsoluteValidator{allowedDomains: allowedDomains}
|
|
}
|
|
|
|
// IsValidRedirect validates that the given redirect string is a valid absolute URL.
|
|
// It must use the 'http' or 'https' scheme.
|
|
// It must point to a host that matches the configured list of allowed domains.
|
|
func (v *AbsoluteValidator) IsValidRedirect(r *http.Request, redirect string) bool {
|
|
u, ok := parsableRequestURI(r, redirect)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if !isRelativeURL(u) && isValidScheme(u) && isAllowedHost(u, v.allowedDomains) {
|
|
return true
|
|
}
|
|
|
|
if isRelativeURL(u) {
|
|
mw.LogEntryFrom(r).Infof("validator: not an absolute URL")
|
|
return false
|
|
}
|
|
|
|
if !isValidScheme(u) {
|
|
mw.LogEntryFrom(r).Infof("validator: invalid scheme; must be one of ['http', 'https']")
|
|
return false
|
|
}
|
|
|
|
if !isAllowedHost(u, v.allowedDomains) {
|
|
mw.LogEntryFrom(r).Infof("validator: host does not match any allowlisted domains: %q", v.allowedDomains)
|
|
return false
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var _ Validator = &RelativeValidator{}
|
|
|
|
type RelativeValidator struct{}
|
|
|
|
func NewRelativeValidator() *RelativeValidator {
|
|
return &RelativeValidator{}
|
|
}
|
|
|
|
// IsValidRedirect validates that the given redirect string is a valid relative URL.
|
|
// It must be an absolute path (i.e. has a leading '/').
|
|
func (v *RelativeValidator) IsValidRedirect(r *http.Request, redirect string) bool {
|
|
u, ok := parsableRequestURI(r, redirect)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if isRelativeURL(u) && isValidAbsolutePath(u.String()) {
|
|
return true
|
|
}
|
|
|
|
mw.LogEntryFrom(r).Infof("validator: not a valid relative URL")
|
|
return false
|
|
}
|
|
|
|
func parsableRequestURI(r *http.Request, redirect string) (*url.URL, bool) {
|
|
if redirect == "" {
|
|
mw.LogEntryFrom(r).Debugf("validator: redirect is empty")
|
|
return nil, false
|
|
}
|
|
|
|
u, err := url.ParseRequestURI(redirect)
|
|
if err != nil {
|
|
mw.LogEntryFrom(r).Infof("validator: %+v", err)
|
|
return nil, false
|
|
}
|
|
|
|
return u, true
|
|
}
|
|
|
|
func isAllowedHost(u *url.URL, allowedDomains []string) bool {
|
|
host := u.Host
|
|
hostname := u.Hostname()
|
|
|
|
if host == "" || hostname == "" || len(allowedDomains) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, allowed := range allowedDomains {
|
|
if isAllowedDomain(u, allowed) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isValidScheme(u *url.URL) bool {
|
|
return u.Scheme == "http" || u.Scheme == "https"
|
|
}
|
|
|
|
func isRelativeURL(u *url.URL) bool {
|
|
return u.Scheme == "" && u.Host == ""
|
|
}
|
|
|
|
func isValidAbsolutePath(redirect string) bool {
|
|
return strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect)
|
|
}
|
|
|
|
func isAllowedDomain(u *url.URL, allowed string) bool {
|
|
if len(allowed) == 0 {
|
|
return false
|
|
}
|
|
|
|
host := u.Host
|
|
hostname := u.Hostname()
|
|
|
|
// exact match on host:port or host
|
|
if host == allowed || hostname == allowed {
|
|
return true
|
|
}
|
|
|
|
// subdomain of allowed domain
|
|
if !strings.HasPrefix(allowed, ".") {
|
|
allowed = "." + allowed
|
|
}
|
|
return strings.HasSuffix(host, allowed)
|
|
}
|