From a5c04308d9cad4fea5ec8d55f7cd6fd53e1c9cce Mon Sep 17 00:00:00 2001 From: oabrivard Date: Sun, 8 Feb 2026 00:07:14 +0100 Subject: [PATCH] Finished Task 9: Infra Security --- backend/shared/infra/security/sanitize.go | 80 +++++++++++++------ .../shared/infra/security/sanitize_test.go | 66 ++++++++++++++- 2 files changed, 121 insertions(+), 25 deletions(-) diff --git a/backend/shared/infra/security/sanitize.go b/backend/shared/infra/security/sanitize.go index 8813822..56785fa 100644 --- a/backend/shared/infra/security/sanitize.go +++ b/backend/shared/infra/security/sanitize.go @@ -8,6 +8,42 @@ import ( "unicode" ) +const ( + // PlayerNameMaxLength defines the maximum length for player names. + PlayerNameMaxLength = 50 + // AnswerMaxLength defines the maximum length for answer submissions. + AnswerMaxLength = 500 + // QuestionTextMaxLength defines the maximum length for admin-authored question text. + QuestionTextMaxLength = 1000 + // ThemeMaxLength defines the maximum length for theme names. + ThemeMaxLength = 100 +) + +var ( + // Sanitization order is trim -> collapse spaces -> HTML escape -> clamp -> allowed pattern. + spaceRegex = regexp.MustCompile(`\s+`) + + // Player names allow letters, numbers, spaces, '-', '_' and '.'. + playerNameAllowedPattern = regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`) + // Themes allow letters, numbers, spaces, '-' and '_'. + themeAllowedPattern = regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`) + + tagRegex = regexp.MustCompile(`<[^>]*>`) + scriptRegex = regexp.MustCompile(`(?i)]*>.*?`) + + dangerousPatterns = []string{ + "javascript:", + "data:", + "vbscript:", + " 0 && len(result) > opts.MaxLength { - result = result[:opts.MaxLength] + if opts.MaxLength > 0 { + result = clampByRunes(result, opts.MaxLength) } // Validate against allowed pattern @@ -66,8 +101,8 @@ func SanitizePlayerName(input string) string { TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, - MaxLength: 50, - AllowedPattern: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`), + MaxLength: PlayerNameMaxLength, + AllowedPattern: playerNameAllowedPattern, } return Sanitize(input, opts) } @@ -78,7 +113,7 @@ func SanitizeAnswer(input string) string { TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, - MaxLength: 500, + MaxLength: AnswerMaxLength, } result := Sanitize(input, opts) @@ -95,13 +130,12 @@ func SanitizeQuestionText(input string) string { TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, - MaxLength: 1000, + MaxLength: QuestionTextMaxLength, } result := Sanitize(input, opts) // Remove potential script content - scriptRegex := regexp.MustCompile(`(?i)]*>.*?`) result = scriptRegex.ReplaceAllString(result, "") return result @@ -113,8 +147,8 @@ func SanitizeTheme(input string) string { TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, - MaxLength: 100, - AllowedPattern: regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`), + MaxLength: ThemeMaxLength, + AllowedPattern: themeAllowedPattern, } result := Sanitize(input, opts) @@ -137,24 +171,11 @@ func SanitizeTheme(input string) string { // RemoveHTMLTags removes all HTML tags from a string. func RemoveHTMLTags(input string) string { - tagRegex := regexp.MustCompile(`<[^>]*>`) return tagRegex.ReplaceAllString(input, "") } // ContainsDangerousPatterns checks if input contains potentially dangerous patterns. func ContainsDangerousPatterns(input string) bool { - dangerousPatterns := []string{ - "javascript:", - "data:", - "vbscript:", - "Hi")) @@ -58,8 +83,29 @@ func TestRemoveHTMLTags(t *testing.T) { // TestContainsDangerousPatterns detects known dangerous substrings. func TestContainsDangerousPatterns(t *testing.T) { - require.True(t, ContainsDangerousPatterns("javascript:alert(1)")) - require.False(t, ContainsDangerousPatterns("hello")) + cases := []struct { + name string + input string + expected bool + }{ + {name: "javascript", input: "javascript:alert(1)", expected: true}, + {name: "mixed_case_javascript", input: "JaVaScRiPt:alert(1)", expected: true}, + {name: "data", input: "data:text/html;base64,abc", expected: true}, + {name: "vbscript", input: "vbscript:msgbox(1)", expected: true}, + {name: "onerror", input: ``, expected: true}, + {name: "onload", input: ``, expected: true}, + {name: "onclick", input: `link`, expected: true}, + {name: "onmouseover", input: `
`, expected: true}, + {name: "benign", input: "hello", expected: false}, + {name: "benign_near_miss_1", input: "onboarding flow", expected: false}, + {name: "benign_near_miss_2", input: "scripture reference", expected: false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, ContainsDangerousPatterns(tc.input)) + }) + } } // TestIsValidEmail verifies basic email pattern validation. @@ -67,3 +113,19 @@ func TestIsValidEmail(t *testing.T) { require.True(t, IsValidEmail("a@b.com")) require.False(t, IsValidEmail("bad@")) } + +// TestSanitizeRuneSafeClamping verifies rune-based truncation remains valid UTF-8 and deterministic. +func TestSanitizeRuneSafeClamping(t *testing.T) { + input := strings.Repeat("é", 5) + opts := SanitizeOptions{ + TrimWhitespace: false, + RemoveMultipleSpaces: false, + HTMLEscape: false, + MaxLength: 3, + } + + result := Sanitize(input, opts) + require.True(t, utf8.ValidString(result)) + require.Equal(t, 3, utf8.RuneCountInString(result)) + require.Equal(t, "ééé", result) +}