The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
241 lines
7.9 KiB
Go
241 lines
7.9 KiB
Go
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
var (
|
|
htmlTagRegex = regexp.MustCompile(`</?[a-zA-Z][^>]*>|<![^>]*>`)
|
|
)
|
|
|
|
// NoHTML validates that a string does not contain HTML tags.
|
|
// It rejects:
|
|
// - HTML tags (e.g., <script>, <b>, <div>, etc.)
|
|
//
|
|
// Plain angle brackets used outside of HTML tags (e.g., "5 < 10") are allowed.
|
|
// This helps prevent XSS attacks and ensures user input doesn't contain HTML markup.
|
|
// Combine with PrintableText() for comprehensive text field validation.
|
|
func NoHTML() ValidatorFunc {
|
|
return func(value any) *ValidationError {
|
|
actualValue, isNil := dereferenceValue(value)
|
|
if isNil {
|
|
return nil
|
|
}
|
|
|
|
str, ok := actualValue.(string)
|
|
if !ok {
|
|
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
|
}
|
|
|
|
if str == "" {
|
|
return nil
|
|
}
|
|
|
|
if htmlTagRegex.MatchString(str) {
|
|
return newValidationError(ErrorCodeInvalidFormat, "must not contain HTML tags")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// PrintableText validates that a string contains only printable UTF-8 characters.
|
|
// It rejects:
|
|
// - Control characters (0x00-0x1F and 0x7F-0x9F, including null bytes and tabs, but allows newlines and carriage returns)
|
|
// - Unicode direction override characters (RLO, LRO, PDF, etc.)
|
|
// - Zero-width characters (ZWSP, ZWNJ, ZWJ, etc.)
|
|
// - Other invisible or formatting characters
|
|
// - Private use area characters
|
|
// - Replacement characters
|
|
//
|
|
// This validator does NOT check for HTML tags - use NoHTML() for that.
|
|
// This validator allows line breaks (newline and carriage return) for multi-line text fields.
|
|
// Use NoNewLine() or SafeTextNoNewLine() for single-line fields that should reject line breaks.
|
|
func PrintableText() ValidatorFunc {
|
|
return func(value any) *ValidationError {
|
|
actualValue, isNil := dereferenceValue(value)
|
|
if isNil {
|
|
return nil
|
|
}
|
|
|
|
str, ok := actualValue.(string)
|
|
if !ok {
|
|
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
|
}
|
|
|
|
if str == "" {
|
|
return nil
|
|
}
|
|
|
|
// Check each rune for invisible or problematic characters
|
|
for i, r := range str {
|
|
// Allow normal space
|
|
if r == ' ' {
|
|
continue
|
|
}
|
|
|
|
// Allow newline (0x0A) and carriage return (0x0D) for multi-line text
|
|
if r == '\n' || r == '\r' {
|
|
continue
|
|
}
|
|
|
|
// Reject control characters (0x00-0x1F and 0x7F-0x9F), except newline and carriage return
|
|
if r < 0x20 || (r >= 0x7F && r < 0xA0) {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invalid control character at position %d", i))
|
|
}
|
|
|
|
// Reject Unicode direction override and formatting characters
|
|
// U+200E LEFT-TO-RIGHT MARK (LRM)
|
|
// U+200F RIGHT-TO-LEFT MARK (RLM)
|
|
// U+202A LEFT-TO-RIGHT EMBEDDING (LRE)
|
|
// U+202B RIGHT-TO-LEFT EMBEDDING (RLE)
|
|
// U+202C POP DIRECTIONAL FORMATTING (PDF)
|
|
// U+202D LEFT-TO-RIGHT OVERRIDE (LRO)
|
|
// U+202E RIGHT-TO-LEFT OVERRIDE (RLO)
|
|
// U+2066 LEFT-TO-RIGHT ISOLATE (LRI)
|
|
// U+2067 RIGHT-TO-LEFT ISOLATE (RLI)
|
|
// U+2068 FIRST STRONG ISOLATE (FSI)
|
|
// U+2069 POP DIRECTIONAL ISOLATE (PDI)
|
|
if r >= 0x200E && r <= 0x200F || r >= 0x202A && r <= 0x202E || r >= 0x2066 && r <= 0x2069 {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains bidirectional override character at position %d", i))
|
|
}
|
|
|
|
// Reject zero-width characters
|
|
// U+200B ZERO WIDTH SPACE (ZWSP)
|
|
// U+200C ZERO WIDTH NON-JOINER (ZWNJ)
|
|
// U+200D ZERO WIDTH JOINER (ZWJ)
|
|
// U+FEFF ZERO WIDTH NO-BREAK SPACE (BOM)
|
|
if r == 0x200B || r == 0x200C || r == 0x200D || r == 0xFEFF {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains zero-width character at position %d", i))
|
|
}
|
|
|
|
// Reject other format characters (Cf category)
|
|
// U+00AD SOFT HYPHEN
|
|
// U+2060 WORD JOINER
|
|
// U+180E MONGOLIAN VOWEL SEPARATOR (deprecated but still problematic)
|
|
if r == 0x00AD || r == 0x2060 || r == 0x180E {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invisible formatting character at position %d", i))
|
|
}
|
|
|
|
// Reject private use area characters (often used for exploits)
|
|
// U+E000-U+F8FF Private Use Area
|
|
// U+F0000-U+FFFFD Supplementary Private Use Area-A
|
|
// U+100000-U+10FFFD Supplementary Private Use Area-B
|
|
if (r >= 0xE000 && r <= 0xF8FF) || (r >= 0xF0000 && r <= 0xFFFFD) || (r >= 0x100000 && r <= 0x10FFFD) {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains private use character at position %d", i))
|
|
}
|
|
|
|
// Reject replacement character (often indicates encoding issues)
|
|
if r == 0xFFFD {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains replacement character at position %d", i))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NoNewLine validates that a string does not contain newline or carriage return characters.
|
|
// It rejects:
|
|
// - Newline characters (\n, 0x0A)
|
|
// - Carriage return characters (\r, 0x0D)
|
|
//
|
|
// This is useful for validating single-line fields like names and titles where line breaks
|
|
// should not be allowed.
|
|
func NoNewLine() ValidatorFunc {
|
|
return func(value any) *ValidationError {
|
|
actualValue, isNil := dereferenceValue(value)
|
|
if isNil {
|
|
return nil
|
|
}
|
|
|
|
str, ok := actualValue.(string)
|
|
if !ok {
|
|
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
|
}
|
|
|
|
if str == "" {
|
|
return nil
|
|
}
|
|
|
|
for i, r := range str {
|
|
if r == '\n' {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains newline character at position %d", i))
|
|
}
|
|
|
|
if r == '\r' {
|
|
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains carriage return character at position %d", i))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// SafeText validates that a string is non-empty, bounded, and contains only safe content.
|
|
// It combines NotEmpty, MaxLen, NoHTML, and PrintableText validators.
|
|
// This allows newlines and carriage returns for multi-line text fields.
|
|
// Use SafeTextNoNewLine for single-line field validation that should reject line breaks.
|
|
func SafeText(maxLen int) ValidatorFunc {
|
|
validators := []ValidatorFunc{
|
|
NotEmpty(),
|
|
MaxLen(maxLen),
|
|
NoHTML(),
|
|
PrintableText(),
|
|
}
|
|
|
|
return func(value any) *ValidationError {
|
|
for _, validator := range validators {
|
|
if err := validator(value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// SafeTextNoNewLine validates that a string is non-empty, bounded, and contains only safe content
|
|
// without newlines or carriage returns. It combines NotEmpty, MaxLen, NoHTML, PrintableText, and NoNewLine validators.
|
|
// This is ideal for validating single-line fields like names, titles, and display names.
|
|
func SafeTextNoNewLine(maxLen int) ValidatorFunc {
|
|
validators := []ValidatorFunc{
|
|
NotEmpty(),
|
|
MaxLen(maxLen),
|
|
NoHTML(),
|
|
PrintableText(),
|
|
NoNewLine(),
|
|
}
|
|
|
|
return func(value any) *ValidationError {
|
|
for _, validator := range validators {
|
|
if err := validator(value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|