Finished Task 9: Infra Security

master
oabrivard 1 month ago
parent 474ebc0801
commit a5c04308d9

@ -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)<script[^>]*>.*?</script>`)
dangerousPatterns = []string{
"javascript:",
"data:",
"vbscript:",
"<script",
"</script",
"onerror",
"onload",
"onclick",
"onmouseover",
}
)
// SanitizeOptions configures sanitization behavior.
type SanitizeOptions struct {
TrimWhitespace bool
@ -38,7 +74,6 @@ func Sanitize(input string, opts SanitizeOptions) string {
// Remove multiple consecutive spaces
if opts.RemoveMultipleSpaces {
spaceRegex := regexp.MustCompile(`\s+`)
result = spaceRegex.ReplaceAllString(result, " ")
}
@ -48,8 +83,8 @@ func Sanitize(input string, opts SanitizeOptions) string {
}
// Apply maximum length
if opts.MaxLength > 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)<script[^>]*>.*?</script>`)
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:",
"<script",
"</script",
"onerror",
"onload",
"onclick",
"onmouseover",
}
lowerInput := strings.ToLower(input)
for _, pattern := range dangerousPatterns {
if strings.Contains(lowerInput, pattern) {
@ -170,3 +191,16 @@ func IsValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
func clampByRunes(input string, max int) string {
if max <= 0 {
return input
}
runes := []rune(input)
if len(runes) <= max {
return input
}
return string(runes[:max])
}

@ -4,7 +4,9 @@ package security
import (
"regexp"
"strings"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/require"
)
@ -51,6 +53,29 @@ func TestSanitizeTheme(t *testing.T) {
require.Equal(t, "Science Fiction", SanitizeTheme(" science fiction "))
}
// TestSanitizeLengthBoundaries verifies exact max length is preserved and over-limit input is truncated.
func TestSanitizeLengthBoundaries(t *testing.T) {
playerExact := strings.Repeat("a", PlayerNameMaxLength)
playerOver := strings.Repeat("a", PlayerNameMaxLength+1)
require.Equal(t, PlayerNameMaxLength, len(SanitizePlayerName(playerExact)))
require.Equal(t, PlayerNameMaxLength, len(SanitizePlayerName(playerOver)))
answerExact := strings.Repeat("x", AnswerMaxLength)
answerOver := strings.Repeat("x", AnswerMaxLength+1)
require.Equal(t, AnswerMaxLength, len(SanitizeAnswer(answerExact)))
require.Equal(t, AnswerMaxLength, len(SanitizeAnswer(answerOver)))
questionExact := strings.Repeat("q", QuestionTextMaxLength)
questionOver := strings.Repeat("q", QuestionTextMaxLength+1)
require.Equal(t, QuestionTextMaxLength, len(SanitizeQuestionText(questionExact)))
require.Equal(t, QuestionTextMaxLength, len(SanitizeQuestionText(questionOver)))
themeExact := strings.Repeat("z", ThemeMaxLength)
themeOver := strings.Repeat("z", ThemeMaxLength+1)
require.Equal(t, ThemeMaxLength, len(SanitizeTheme(themeExact)))
require.Equal(t, ThemeMaxLength, len(SanitizeTheme(themeOver)))
}
// TestRemoveHTMLTags verifies all HTML tags are stripped from input.
func TestRemoveHTMLTags(t *testing.T) {
require.Equal(t, "Hi", RemoveHTMLTags("<b>Hi</b>"))
@ -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: `<img src=x onerror="alert(1)">`, expected: true},
{name: "onload", input: `<body onload="x()">`, expected: true},
{name: "onclick", input: `<a onclick="x()">link</a>`, expected: true},
{name: "onmouseover", input: `<div onmouseover="x()">`, 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)
}

Loading…
Cancel
Save