Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,263 @@
package inputvalidation
import (
"fmt"
"net/mail"
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// Validation limits for input fields
const (
// Email limits
MaxEmailLength = 254 // RFC 5321
// Name limits (collection names, file names, user names)
MinNameLength = 1
MaxNameLength = 255
// Display name limits
MaxDisplayNameLength = 100
// Description limits
MaxDescriptionLength = 1000
// UUID format (standard UUID v4)
UUIDLength = 36
// OTT (One-Time Token) limits
OTTLength = 8 // 8-digit code
// Password limits
MinPasswordLength = 8
MaxPasswordLength = 128
)
// uuidRegex matches standard UUID format (8-4-4-4-12)
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
// ottRegex matches 8-digit OTT codes
var ottRegex = regexp.MustCompile(`^[0-9]{8}$`)
// ValidateEmail validates an email address
func ValidateEmail(email string) error {
if email == "" {
return fmt.Errorf("email is required")
}
// Check length
if len(email) > MaxEmailLength {
return fmt.Errorf("email exceeds maximum length of %d characters", MaxEmailLength)
}
// Use Go's mail package for RFC 5322 validation
_, err := mail.ParseAddress(email)
if err != nil {
return fmt.Errorf("invalid email format")
}
// Additional checks for security
if strings.ContainsAny(email, "\x00\n\r") {
return fmt.Errorf("email contains invalid characters")
}
return nil
}
// ValidateUUID validates a UUID string
func ValidateUUID(id, fieldName string) error {
if id == "" {
return fmt.Errorf("%s is required", fieldName)
}
if len(id) != UUIDLength {
return fmt.Errorf("%s must be a valid UUID", fieldName)
}
if !uuidRegex.MatchString(id) {
return fmt.Errorf("%s must be a valid UUID format", fieldName)
}
return nil
}
// ValidateName validates a name field (collection name, filename, etc.)
func ValidateName(name, fieldName string) error {
if name == "" {
return fmt.Errorf("%s is required", fieldName)
}
// Check length
if len(name) > MaxNameLength {
return fmt.Errorf("%s exceeds maximum length of %d characters", fieldName, MaxNameLength)
}
// Check for valid UTF-8
if !utf8.ValidString(name) {
return fmt.Errorf("%s contains invalid characters", fieldName)
}
// Check for control characters (except tab and newline which might be valid in descriptions)
for _, r := range name {
if r < 32 && r != '\t' && r != '\n' && r != '\r' {
return fmt.Errorf("%s contains invalid control characters", fieldName)
}
// Also check for null byte and other dangerous characters
if r == 0 {
return fmt.Errorf("%s contains null characters", fieldName)
}
}
// Check that it's not all whitespace
if strings.TrimSpace(name) == "" {
return fmt.Errorf("%s cannot be empty or whitespace only", fieldName)
}
return nil
}
// ValidateDisplayName validates a display name (first name, last name, etc.)
func ValidateDisplayName(name, fieldName string) error {
// Display names can be empty (optional fields)
if name == "" {
return nil
}
// Check length
if len(name) > MaxDisplayNameLength {
return fmt.Errorf("%s exceeds maximum length of %d characters", fieldName, MaxDisplayNameLength)
}
// Check for valid UTF-8
if !utf8.ValidString(name) {
return fmt.Errorf("%s contains invalid characters", fieldName)
}
// Check for control characters
for _, r := range name {
if r < 32 || !unicode.IsPrint(r) {
if r != ' ' { // Allow spaces
return fmt.Errorf("%s contains invalid characters", fieldName)
}
}
}
return nil
}
// ValidateDescription validates a description field
func ValidateDescription(desc string) error {
// Descriptions can be empty (optional)
if desc == "" {
return nil
}
// Check length
if len(desc) > MaxDescriptionLength {
return fmt.Errorf("description exceeds maximum length of %d characters", MaxDescriptionLength)
}
// Check for valid UTF-8
if !utf8.ValidString(desc) {
return fmt.Errorf("description contains invalid characters")
}
// Check for null bytes
if strings.ContainsRune(desc, 0) {
return fmt.Errorf("description contains null characters")
}
return nil
}
// ValidateOTT validates a one-time token (8-digit code)
func ValidateOTT(ott string) error {
if ott == "" {
return fmt.Errorf("verification code is required")
}
// Trim whitespace (users might copy-paste with spaces)
ott = strings.TrimSpace(ott)
if !ottRegex.MatchString(ott) {
return fmt.Errorf("verification code must be an 8-digit number")
}
return nil
}
// ValidatePassword validates a password
func ValidatePassword(password string) error {
if password == "" {
return fmt.Errorf("password is required")
}
if len(password) < MinPasswordLength {
return fmt.Errorf("password must be at least %d characters", MinPasswordLength)
}
if len(password) > MaxPasswordLength {
return fmt.Errorf("password exceeds maximum length of %d characters", MaxPasswordLength)
}
// Check for null bytes (could indicate injection attempt)
if strings.ContainsRune(password, 0) {
return fmt.Errorf("password contains invalid characters")
}
return nil
}
// ValidateCollectionID is a convenience function for collection ID validation
func ValidateCollectionID(id string) error {
return ValidateUUID(id, "collection ID")
}
// ValidateFileID is a convenience function for file ID validation
func ValidateFileID(id string) error {
return ValidateUUID(id, "file ID")
}
// ValidateTagID is a convenience function for tag ID validation
func ValidateTagID(id string) error {
return ValidateUUID(id, "tag ID")
}
// ValidateCollectionName validates a collection name
func ValidateCollectionName(name string) error {
return ValidateName(name, "collection name")
}
// ValidateFileName validates a file name
func ValidateFileName(name string) error {
if err := ValidateName(name, "filename"); err != nil {
return err
}
// Additional file-specific validations
// Check for path traversal attempts
if strings.Contains(name, "..") {
return fmt.Errorf("filename cannot contain path traversal sequences")
}
// Check for path separators
if strings.ContainsAny(name, "/\\") {
return fmt.Errorf("filename cannot contain path separators")
}
return nil
}
// SanitizeString removes or replaces potentially dangerous characters
// This is a defense-in-depth measure - validation should be done first
func SanitizeString(s string) string {
// Remove null bytes
s = strings.ReplaceAll(s, "\x00", "")
// Trim excessive whitespace
s = strings.TrimSpace(s)
return s
}

View file

@ -0,0 +1,167 @@
package inputvalidation
import (
"fmt"
"net"
"net/url"
"strings"
)
// AllowedDownloadHosts lists the allowed hosts for presigned download URLs.
// These are the only hosts from which the application will download files.
var AllowedDownloadHosts = []string{
// Production S3-compatible storage (Digital Ocean Spaces)
".digitaloceanspaces.com",
// AWS S3 (if used in future)
".s3.amazonaws.com",
".s3.us-east-1.amazonaws.com",
".s3.us-west-2.amazonaws.com",
".s3.eu-west-1.amazonaws.com",
// MapleFile domains (if serving files directly)
".maplefile.ca",
// Local development
"localhost",
"127.0.0.1",
}
// ValidateDownloadURL validates a presigned download URL before use.
// This prevents SSRF attacks by ensuring downloads only happen from trusted hosts.
func ValidateDownloadURL(rawURL string) error {
if rawURL == "" {
return fmt.Errorf("download URL is required")
}
// Parse the URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}
// Validate scheme - must be HTTPS (except localhost for development)
if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" {
return fmt.Errorf("URL must use HTTP or HTTPS scheme")
}
// Get host without port
host := parsedURL.Hostname()
if host == "" {
return fmt.Errorf("URL must have a valid host")
}
// For HTTPS requirement - only allow HTTP for localhost/local IPs
if parsedURL.Scheme == "http" {
if !isLocalHost(host) {
return fmt.Errorf("non-local URLs must use HTTPS")
}
}
// Check if host is in allowed list
if !isAllowedHost(host) {
return fmt.Errorf("download from host %q is not allowed", host)
}
// Check for credentials in URL (security risk)
if parsedURL.User != nil {
return fmt.Errorf("URL must not contain credentials")
}
// Check for suspicious path traversal in URL path
if strings.Contains(parsedURL.Path, "..") {
return fmt.Errorf("URL path contains invalid sequences")
}
return nil
}
// isAllowedHost checks if a host is in the allowed download hosts list
func isAllowedHost(host string) bool {
host = strings.ToLower(host)
for _, allowed := range AllowedDownloadHosts {
allowed = strings.ToLower(allowed)
// Exact match
if host == allowed {
return true
}
// Suffix match for wildcard domains (e.g., ".digitaloceanspaces.com")
if strings.HasPrefix(allowed, ".") && strings.HasSuffix(host, allowed) {
return true
}
// Handle subdomains for non-wildcard entries
if !strings.HasPrefix(allowed, ".") {
if host == allowed || strings.HasSuffix(host, "."+allowed) {
return true
}
}
}
return false
}
// isLocalHost checks if a host is localhost or a local IP address
func isLocalHost(host string) bool {
host = strings.ToLower(host)
// Check common localhost names
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return true
}
// Check if it's a local network IP
ip := net.ParseIP(host)
if ip == nil {
return false
}
// Check for loopback
if ip.IsLoopback() {
return true
}
// Check for private network ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
if ip.IsPrivate() {
return true
}
return false
}
// ValidateAPIBaseURL validates a base URL for API requests
func ValidateAPIBaseURL(rawURL string) error {
if rawURL == "" {
return fmt.Errorf("API URL is required")
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}
// Validate scheme
if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" {
return fmt.Errorf("URL must use HTTP or HTTPS scheme")
}
// Get host
host := parsedURL.Hostname()
if host == "" {
return fmt.Errorf("URL must have a valid host")
}
// For HTTPS requirement - only allow HTTP for localhost/local IPs
if parsedURL.Scheme == "http" {
if !isLocalHost(host) {
return fmt.Errorf("non-local URLs must use HTTPS")
}
}
// Check for credentials in URL
if parsedURL.User != nil {
return fmt.Errorf("URL must not contain credentials")
}
return nil
}