43 lines
1.1 KiB
Go
43 lines
1.1 KiB
Go
package shorturl
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// MaxPathLength caps the long URL length we'll accept. 2KB is well beyond any
|
|
// real `?hl=` URL we generate while preventing pathological storage abuse.
|
|
const MaxPathLength = 2048
|
|
|
|
// ValidatePath verifies that s is a same-site relative URL — starts with "/",
|
|
// no scheme/host, no NUL bytes, within length. Prevents using the shortener
|
|
// as an open redirect to arbitrary external domains.
|
|
func ValidatePath(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("url is empty")
|
|
}
|
|
if len(s) > MaxPathLength {
|
|
return fmt.Errorf("url exceeds %d bytes", MaxPathLength)
|
|
}
|
|
if !strings.HasPrefix(s, "/") {
|
|
return fmt.Errorf("url must start with '/'")
|
|
}
|
|
// "//host/…" is protocol-relative — disallow.
|
|
if strings.HasPrefix(s, "//") {
|
|
return fmt.Errorf("url must not be protocol-relative")
|
|
}
|
|
if strings.ContainsRune(s, 0) {
|
|
return fmt.Errorf("url contains NUL byte")
|
|
}
|
|
// No scheme allowed: scan up to the first '/' or '?' for ':'.
|
|
for i, c := range s {
|
|
if c == '?' || c == '#' {
|
|
break
|
|
}
|
|
if c == ':' && i > 0 {
|
|
return fmt.Errorf("url must not contain a scheme")
|
|
}
|
|
}
|
|
return nil
|
|
}
|