Files
user-system/internal/security/validator.go
long-agent 2a18a6fb47 fix(n+1): 批量查询替代循环单查
- IsAdminBootstrapRequired: userRepo.GetByID 循环 → GetByIDs 批量
- AssignRoles: roleRepo.GetByID 循环 → GetByIDs 批量
- 在 userRepositoryInterface 补充 GetByIDs 方法签名
2026-05-08 08:05:26 +08:00

193 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package security
import (
"net"
"regexp"
"strings"
)
// Validator groups lightweight validation and sanitization helpers.
type Validator struct {
passwordMinLength int
passwordRequireSpecial bool
passwordRequireNumber bool
// 预编译的正则表达式避免每次调用重复编译P1性能优化
emailRe *regexp.Regexp
phoneRe *regexp.Regexp
usernameRe *regexp.Regexp
urlRe *regexp.Regexp
sqlPatterns []*regexp.Regexp
xssPatterns []*regexp.Regexp
}
// NewValidator creates a validator with the configured password rules.
func NewValidator(minLength int, requireSpecial, requireNumber bool) *Validator {
v := &Validator{
passwordMinLength: minLength,
passwordRequireSpecial: requireSpecial,
passwordRequireNumber: requireNumber,
}
// 预编译常用验证正则P1性能优化
v.emailRe = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
v.phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
v.usernameRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,19}$`)
v.urlRe = regexp.MustCompile(`^https?://[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=]+$`)
// 预编译SQL注入检测正则P1性能优化
sqlRawPatterns := []string{
`;[\s]*--`,
`/\*.*?\*/`,
`\bxp_\w+`,
`\bexec[\s\(]`,
`\bsp_\w+`,
`\bwaitfor[\s]+delay`,
`\bunion[\s]+select`,
`\bdrop[\s]+table`,
`\binsert[\s]+into`,
`\bupdate[\s]+\w+[\s]+set`,
`\bdelete[\s]+from`,
}
v.sqlPatterns = make([]*regexp.Regexp, len(sqlRawPatterns))
for i, p := range sqlRawPatterns {
v.sqlPatterns[i] = regexp.MustCompile(`(?i)` + p)
}
// 预编译XSS检测正则P1性能优化
xssRawPatterns := []string{
`(?i)<script[^>]*>.*?</script>`,
`(?i)</script>`,
`(?i)<iframe[^>]*>.*?</iframe>`,
`(?i)<object[^>]*>.*?</object>`,
`(?i)<embed[^>]*>.*?</embed>`,
`(?i)<applet[^>]*>.*?</applet>`,
`(?i)javascript\s*:`,
`(?i)vbscript\s*:`,
`(?i)data\s*:`,
`(?i)on\w+\s*=`,
`(?i)<style[^>]*>.*?</style>`,
}
v.xssPatterns = make([]*regexp.Regexp, len(xssRawPatterns))
for i, p := range xssRawPatterns {
v.xssPatterns[i] = regexp.MustCompile(p)
}
return v
}
// ValidateEmail validates email format.
func (v *Validator) ValidateEmail(email string) bool {
if email == "" || v.emailRe == nil {
return false
}
return v.emailRe.MatchString(email)
}
// ValidatePhone validates mainland China mobile numbers.
func (v *Validator) ValidatePhone(phone string) bool {
if phone == "" || v.phoneRe == nil {
return false
}
return v.phoneRe.MatchString(phone)
}
// ValidateUsername validates usernames.
func (v *Validator) ValidateUsername(username string) bool {
if username == "" || v.usernameRe == nil {
return false
}
return v.usernameRe.MatchString(username)
}
// ValidatePassword validates passwords using the shared runtime policy.
func (v *Validator) ValidatePassword(password string) bool {
policy := PasswordPolicy{
MinLength: v.passwordMinLength,
RequireSpecial: v.passwordRequireSpecial,
RequireNumber: v.passwordRequireNumber,
}
return policy.Validate(password) == nil
}
// SanitizeSQL removes obviously dangerous SQL injection patterns using regex.
// This is a defense-in-depth measure; parameterized queries should always be used.
func (v *Validator) SanitizeSQL(input string) string {
// Escape SQL special characters by doubling them (SQL standard approach)
// Order matters: escape backslash first to avoid double-escaping
replacer := strings.NewReplacer(
`\`, `\\`,
`'`, `''`,
`"`, `""`,
)
result := replacer.Replace(input)
// 使用预编译的正则移除SQL注入模式P1性能优化
for _, re := range v.sqlPatterns {
if re != nil {
result = re.ReplaceAllString(result, "")
}
}
return result
}
// SanitizeXSS removes obviously dangerous XSS patterns using regex.
// This is a defense-in-depth measure; output encoding should always be used.
func (v *Validator) SanitizeXSS(input string) string {
result := input
// 使用预编译的正则移除XSS模式P1性能优化
for _, re := range v.xssPatterns {
if re != nil {
result = re.ReplaceAllString(result, "")
}
}
// Encode < and > to prevent tag construction
result = strings.ReplaceAll(result, "<", "&lt;")
result = strings.ReplaceAll(result, ">", "&gt;")
// Restore entities if they were part of legitimate content
result = strings.ReplaceAll(result, "&lt;", "<")
result = strings.ReplaceAll(result, "&gt;", ">")
return result
}
// ValidateURL validates a basic HTTP/HTTPS URL.
func (v *Validator) ValidateURL(url string) bool {
if url == "" || v.urlRe == nil {
return false
}
return v.urlRe.MatchString(url)
}
// ValidateIP validates IPv4 or IPv6 addresses using net.ParseIP.
// Supports all valid formats including compressed IPv6 (::1, fe80::1, etc.)
func (v *Validator) ValidateIP(ip string) bool {
if ip == "" {
return false
}
return net.ParseIP(ip) != nil
}
// ValidateIPv4 validates IPv4 addresses only.
func (v *Validator) ValidateIPv4(ip string) bool {
if ip == "" {
return false
}
parsed := net.ParseIP(ip)
return parsed != nil && parsed.To4() != nil
}
// ValidateIPv6 validates IPv6 addresses only.
func (v *Validator) ValidateIPv6(ip string) bool {
if ip == "" {
return false
}
parsed := net.ParseIP(ip)
return parsed != nil && parsed.To4() == nil
}