Allow more permissive bracket validations

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-16 17:05:40 +01:00
parent f2d307ac1a
commit 06aa781bb2
8 changed files with 142 additions and 132 deletions

View File

@@ -182,13 +182,6 @@ func TestAudit_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
input: map[string]any{
"name": "Test < Audit",
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
input: map[string]any{
@@ -425,14 +418,6 @@ func TestAudit_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
setup: func() string { return baseAuditID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test < Audit"}
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
setup: func() string { return baseAuditID },

View File

@@ -174,14 +174,6 @@ func TestDatum_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
input: map[string]any{
"name": "Test < Datum",
"dataClassification": "INTERNAL",
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
input: map[string]any{
@@ -397,14 +389,6 @@ func TestDatum_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
setup: func() string { return baseDatumID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test < Datum"}
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
setup: func() string { return baseDatumID },

View File

@@ -192,16 +192,6 @@ func TestDocument_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "title with angle brackets",
input: map[string]any{
"title": "Test < Document",
"content": "Test content",
"documentType": "POLICY",
"classification": "INTERNAL",
},
wantErrorContains: "angle brackets",
},
{
name: "title with newline",
input: map[string]any{
@@ -418,14 +408,6 @@ func TestDocument_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "title with angle brackets",
setup: func() string { return baseDocumentID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "title": "Test < Document"}
},
wantErrorContains: "angle brackets",
},
{
name: "title with newline",
setup: func() string { return baseDocumentID },

View File

@@ -144,13 +144,6 @@ func TestFramework_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
input: map[string]any{
"name": "Test < Framework",
},
wantErrorContains: "angle brackets",
},
// Newline validation
{
name: "name with newline",
@@ -359,14 +352,6 @@ func TestFramework_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
setup: func() string { return baseFrameworkID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test < Framework"}
},
wantErrorContains: "angle brackets",
},
{
name: "description with HTML tags",
setup: func() string { return baseFrameworkID },

View File

@@ -205,14 +205,6 @@ func TestMeasure_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
input: map[string]any{
"name": "Test < Measure",
"category": "POLICY",
},
wantErrorContains: "angle brackets",
},
{
name: "description with HTML tags",
input: map[string]any{
@@ -473,14 +465,6 @@ func TestMeasure_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test < Measure"}
},
wantErrorContains: "angle brackets",
},
{
name: "description with HTML tags",
setup: func() string { return baseMeasureID },

View File

@@ -135,14 +135,6 @@ func TestMeeting_Create_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
input: map[string]any{
"name": "Test < Meeting",
"date": time.Now().Format(time.RFC3339Nano),
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
input: map[string]any{
@@ -343,14 +335,6 @@ func TestMeeting_Update_Validation(t *testing.T) {
},
wantErrorContains: "HTML",
},
{
name: "name with angle brackets",
setup: func() string { return baseMeetingID },
input: func(id string) map[string]any {
return map[string]any{"meetingId": id, "name": "Test < Meeting"}
},
wantErrorContains: "angle brackets",
},
{
name: "name with newline",
setup: func() string { return baseMeetingID },

View File

@@ -17,18 +17,17 @@ package validator
import (
"fmt"
"regexp"
"strings"
)
var (
htmlTagRegex = regexp.MustCompile(`<[^>]*>`)
htmlTagRegex = regexp.MustCompile(`</?[a-zA-Z][^>]*>|<![^>]*>`)
)
// NoHTML validates that a string does not contain HTML tags or angle brackets.
// NoHTML validates that a string does not contain HTML tags.
// It rejects:
// - HTML tags (e.g., <script>, <b>, <div>, etc.)
// - Angle brackets (< and >) even when not part of complete tags
//
// 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 {
@@ -47,16 +46,10 @@ func NoHTML() ValidatorFunc {
return nil
}
// Check for HTML tags first (more specific error message)
if htmlTagRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must not contain HTML tags")
}
// Check for angle brackets (even without complete tags)
if strings.ContainsAny(str, "<>") {
return newValidationError(ErrorCodeInvalidFormat, "must not contain angle brackets")
}
return nil
}
}

View File

@@ -106,38 +106,35 @@ func TestNoHTML(t *testing.T) {
}
})
t.Run("invalid - less than symbol", func(t *testing.T) {
t.Run("valid - less than symbol", func(t *testing.T) {
str := "5 < 10"
err := NoHTML()(&str)
if err == nil {
t.Fatal("expected validation error for angle bracket")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
if err != nil {
t.Errorf("expected no error for bare angle bracket, got: %v", err)
}
})
t.Run("invalid - greater than symbol", func(t *testing.T) {
t.Run("valid - greater than symbol", func(t *testing.T) {
str := "10 > 5"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle bracket")
if err != nil {
t.Errorf("expected no error for bare angle bracket, got: %v", err)
}
})
t.Run("invalid - both angle brackets", func(t *testing.T) {
t.Run("valid - both angle brackets", func(t *testing.T) {
str := "5 < x > 10"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle brackets")
if err != nil {
t.Errorf("expected no error for bare angle brackets, got: %v", err)
}
})
t.Run("invalid - malformed tag", func(t *testing.T) {
t.Run("valid - incomplete tag", func(t *testing.T) {
str := "text <incomplete"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for incomplete tag")
if err != nil {
t.Errorf("expected no error for incomplete tag, got: %v", err)
}
})
@@ -149,6 +146,126 @@ func TestNoHTML(t *testing.T) {
}
})
t.Run("invalid - svg onload XSS", func(t *testing.T) {
str := `<svg onload=alert(1)>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for svg tag")
}
})
t.Run("invalid - svg with slash", func(t *testing.T) {
str := `<svg/onload=alert(1)>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for svg/onload tag")
}
})
t.Run("invalid - iframe tag", func(t *testing.T) {
str := `<iframe src="javascript:alert(1)">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for iframe tag")
}
})
t.Run("invalid - style tag", func(t *testing.T) {
str := `<style>body{background:url(evil)}</style>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for style tag")
}
})
t.Run("invalid - HTML comment", func(t *testing.T) {
str := `<!-- comment -->`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for HTML comment")
}
})
t.Run("invalid - DOCTYPE", func(t *testing.T) {
str := `<!DOCTYPE html>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for DOCTYPE")
}
})
t.Run("invalid - details ontoggle XSS", func(t *testing.T) {
str := `<details open ontoggle=alert(1)>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for details tag")
}
})
t.Run("invalid - body onload XSS", func(t *testing.T) {
str := `<body onload=alert(1)>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for body tag")
}
})
t.Run("invalid - object tag", func(t *testing.T) {
str := `<object data="evil.swf">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for object tag")
}
})
t.Run("invalid - embed tag", func(t *testing.T) {
str := `<embed src="evil.swf">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for embed tag")
}
})
t.Run("invalid - meta refresh", func(t *testing.T) {
str := `<meta http-equiv="refresh" content="0;url=evil">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for meta tag")
}
})
t.Run("invalid - input with autofocus XSS", func(t *testing.T) {
str := `<input onfocus=alert(1) autofocus>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for input tag")
}
})
t.Run("invalid - tag with newlines in attributes", func(t *testing.T) {
str := "<img\nsrc=x\nonerror=alert(1)>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for tag with newlines")
}
})
t.Run("valid - math expression", func(t *testing.T) {
str := "if x < 10 then y = 20"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error for math expression, got: %v", err)
}
})
t.Run("valid - arrow notation", func(t *testing.T) {
str := "use -> or => for arrows"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error for arrow notation, got: %v", err)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := NoHTML()(&str)
@@ -209,13 +326,13 @@ func TestNoHTML(t *testing.T) {
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
if strings.Contains(err.Message, "HTML tags") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
t.Error("expected error about HTML tags")
}
})
@@ -623,14 +740,11 @@ func TestSafeText(t *testing.T) {
}
})
t.Run("invalid - contains angle brackets", func(t *testing.T) {
t.Run("valid - contains angle brackets", func(t *testing.T) {
str := "5 < 10"
err := SafeText(100)(&str)
if err == nil {
t.Fatal("expected validation error for angle brackets")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
if err != nil {
t.Errorf("expected no error for bare angle brackets, got: %v", err)
}
})
@@ -743,13 +857,13 @@ func TestSafeText(t *testing.T) {
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
if strings.Contains(err.Message, "HTML tags") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
t.Error("expected error about HTML tags")
}
})
@@ -922,4 +1036,3 @@ func TestSafeTextNoNewLine(t *testing.T) {
}
})
}