Files
Super-HomeworkManager/internal/validation/validation.go
2026-01-13 09:56:50 +09:00

202 lines
6.9 KiB
Go

package validation
import (
"fmt"
"regexp"
"strings"
"unicode"
)
var MaxLengths = map[string]int{
"title": 200,
"description": 5000,
"subject": 100,
"priority": 20,
}
var xssPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)<\s*script`), // <script with optional space
regexp.MustCompile(`(?i)</\s*script`), // </script
regexp.MustCompile(`(?i)javascript\s*:`), // javascript:
regexp.MustCompile(`(?i)on\w+\s*=`), // onclick=, onerror=, etc.
regexp.MustCompile(`(?i)<\s*iframe`), // <iframe
regexp.MustCompile(`(?i)<\s*object`), // <object
regexp.MustCompile(`(?i)<\s*embed`), // <embed
regexp.MustCompile(`(?i)<\s*svg[^>]*on\w+\s*=`), // <svg with event handler
regexp.MustCompile(`(?i)data\s*:\s*text/html`), // data:text/html
regexp.MustCompile(`(?i)<\s*img[^>]*on\w+\s*=`), // <img with event handler
regexp.MustCompile(`(?i)expression\s*\(`), // CSS expression()
regexp.MustCompile(`(?i)alert\s*\(`), // alert()
regexp.MustCompile(`(?i)confirm\s*\(`), // confirm()
regexp.MustCompile(`(?i)prompt\s*\(`), // prompt()
regexp.MustCompile(`(?i)document\s*\.\s*cookie`), // document.cookie
regexp.MustCompile(`(?i)document\s*\.\s*location`), // document.location
regexp.MustCompile(`(?i)window\s*\.\s*location`), // window.location
}
// SQL injection detection patterns (common attack signatures)
var sqlInjectionPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)'\s*or\s+`), // ' OR (simplified)
regexp.MustCompile(`(?i)'\s*and\s+`), // ' AND
regexp.MustCompile(`(?i)"\s*or\s+`), // " OR
regexp.MustCompile(`(?i)"\s*and\s+`), // " AND
regexp.MustCompile(`(?i)union\s+(all\s+)?select`), // UNION SELECT
regexp.MustCompile(`(?i);\s*(drop|delete|update|insert|alter|truncate)\s+`), // ; DROP etc
regexp.MustCompile(`(?i)--\s*$`), // SQL comment at end
regexp.MustCompile(`(?i)/\*.*\*/`), // SQL block comment
regexp.MustCompile(`(?i)'\s*;\s*`), // '; (statement termination)
regexp.MustCompile(`(?i)exec\s*\(`), // EXEC(
regexp.MustCompile(`(?i)xp_\w+`), // xp_cmdshell etc.
regexp.MustCompile(`(?i)load_file\s*\(`), // MySQL LOAD_FILE
regexp.MustCompile(`(?i)into\s+(out|dump)file`), // MySQL file operations
regexp.MustCompile(`(?i)benchmark\s*\(`), // MySQL BENCHMARK
regexp.MustCompile(`(?i)sleep\s*\(\s*\d`), // SLEEP()
regexp.MustCompile(`(?i)waitfor\s+delay`), // WAITFOR DELAY
regexp.MustCompile(`(?i)1\s*=\s*1`), // 1=1
regexp.MustCompile(`(?i)'1'\s*=\s*'1`), // '1'='1
}
// Path traversal detection patterns
var pathTraversalPatterns = []*regexp.Regexp{
regexp.MustCompile(`\.\.[\\/]`),
regexp.MustCompile(`\.\.%2[fF]`),
regexp.MustCompile(`%2e%2e[\\/]`),
regexp.MustCompile(`\.\./`), // ../
regexp.MustCompile(`\.\.\\`), // ..\
}
// Command injection detection patterns
var commandInjectionPatterns = []*regexp.Regexp{
regexp.MustCompile(`^\s*;`), // starts with semicolon
regexp.MustCompile(`;\s*\w+`), // ; followed by command
regexp.MustCompile(`\|\s*\w+`), // | pipe to command
regexp.MustCompile("`[^`]+`"), // backtick execution
regexp.MustCompile(`\$\([^)]+\)`), // $(command) execution
regexp.MustCompile(`&&\s*\w+`), // && chained command
regexp.MustCompile(`\|\|\s*\w+`), // || chained command
}
// ValidationError represents a validation failure
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidateAssignmentInput validates assignment creation/update input
func ValidateAssignmentInput(title, description, subject, priority string) error {
// Validate title
if err := ValidateField("title", title, true); err != nil {
return err
}
// Validate description
if err := ValidateField("description", description, false); err != nil {
return err
}
// Validate subject
if err := ValidateField("subject", subject, false); err != nil {
return err
}
// Validate priority
if err := ValidateField("priority", priority, false); err != nil {
return err
}
return nil
}
// ValidateField validates a single field
func ValidateField(fieldName, value string, required bool) error {
// Check required
if required && strings.TrimSpace(value) == "" {
return &ValidationError{Field: fieldName, Message: "必須項目です"}
}
// Skip further validation if empty and not required
if value == "" {
return nil
}
// Check max length
if maxLen, ok := MaxLengths[fieldName]; ok {
if len(value) > maxLen {
return &ValidationError{
Field: fieldName,
Message: fmt.Sprintf("最大%d文字までです", maxLen),
}
}
}
// Check for control characters (except newline in description)
if fieldName != "description" {
for _, r := range value {
if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' {
return &ValidationError{
Field: fieldName,
Message: "不正な制御文字が含まれています",
}
}
}
}
// Check for XSS patterns
for _, pattern := range xssPatterns {
if pattern.MatchString(value) {
return &ValidationError{
Field: fieldName,
Message: "潜在的に危険なHTMLタグまたはスクリプトが含まれています",
}
}
}
// Check for SQL injection patterns
for _, pattern := range sqlInjectionPatterns {
if pattern.MatchString(value) {
return &ValidationError{
Field: fieldName,
Message: "潜在的に危険なSQL構文が含まれています",
}
}
}
// Check for path traversal patterns
for _, pattern := range pathTraversalPatterns {
if pattern.MatchString(value) {
return &ValidationError{
Field: fieldName,
Message: "不正なパス文字列が含まれています",
}
}
}
// Check for command injection patterns
for _, pattern := range commandInjectionPatterns {
if pattern.MatchString(value) {
return &ValidationError{
Field: fieldName,
Message: "潜在的に危険なコマンド構文が含まれています",
}
}
}
return nil
}
// SanitizeString removes potentially dangerous characters while preserving readability
// This is a secondary defense - validation should catch issues first
func SanitizeString(s string) string {
// Remove null bytes
s = strings.ReplaceAll(s, "\x00", "")
// Normalize whitespace
s = strings.TrimSpace(s)
return s
}