package wafenginecore import ( "SamWaf/common/zlog" "SamWaf/global" "SamWaf/innerbean" "SamWaf/model" "SamWaf/model/wafenginmodel" "go.uber.org/zap" "net/http" "os" "path" "path/filepath" "regexp" "strings" "time" ) // 敏感文件和路径黑名单 var ( // 路径穿越攻击模式 pathTraversalPatterns = []*regexp.Regexp{ regexp.MustCompile(`\.\.[\\/]`), regexp.MustCompile(`[\\/]\.\.[\\/]`), regexp.MustCompile(`%2e%2e[\\/]`), regexp.MustCompile(`%2e%2e%2f`), regexp.MustCompile(`%2e%2e%5c`), regexp.MustCompile(`\.\.[\\/%]`), regexp.MustCompile(`%252e%252e`), regexp.MustCompile(`%c0%ae%c0%ae`), regexp.MustCompile(`%uff0e%uff0e`), regexp.MustCompile(`[\\/]{2,}`), regexp.MustCompile(`\\{2,}`), } ) // splitConfigString 分割配置字符串为数组,去除空白项 func splitConfigString(configStr string) []string { if configStr == "" { return []string{} } parts := strings.Split(configStr, ",") result := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { result = append(result, part) } } return result } // getDefaultSensitivePathsString 获取默认敏感路径配置字符串 func getDefaultSensitivePathsString() string { return "/etc/passwd,/etc/shadow,/etc/group,/etc/gshadow,/etc/hosts,/etc/hostname,/etc/resolv.conf,/etc/ssh/,/var/log/,/.ssh/,/.bash_history,/.profile,/.bashrc,/etc/crontab,/var/spool/cron/,/etc/apache2/,/etc/nginx/,/etc/httpd/,/var/www/,/usr/share/,/var/tmp/,/var/run/,c:\\windows\\,c:\\program files\\,c:\\program files (x86)\\,c:\\users\\,c:\\documents and settings\\,c:\\windows\\system32\\,c:\\windows\\syswow64\\,c:\\boot.ini,c:\\autoexec.bat,c:\\config.sys,\\windows\\,\\program files\\,\\program files (x86)\\,\\users\\,\\documents and settings\\,\\windows\\system32\\,\\windows\\syswow64\\,boot.ini,autoexec.bat,config.sys,ntuser.dat,pagefile.sys,hiberfil.sys,swapfile.sys" } // getDefaultSensitiveExtensionsString 获取默认敏感文件扩展名字符串 func getDefaultSensitiveExtensionsString() string { return ".key,.pem,.crt,.p12,.pfx,.jks,.bak,.backup,.old,.orig,.save,.sql,.db,.sqlite,.mdb,.env,.htaccess,.htpasswd,.git,.svn,.hg,.bzr,.DS_Store,Thumbs.db,desktop.ini,.tmp,.temp,.lock,.pid" } // getDefaultAllowedExtensionsString 获取默认允许的文件扩展名字符串 func getDefaultAllowedExtensionsString() string { return ".html,.htm,.css,.js,.json,.png,.jpg,.jpeg,.gif,.svg,.ico,.webp,.pdf,.txt,.md,.xml,.woff,.woff2,.ttf,.eot,.mp4,.webm,.ogg,.mp3,.wav,.zip,.tar,.gz,.rar" } // getDefaultSensitivePatternsString 获取默认敏感文件名模式字符串 func getDefaultSensitivePatternsString() string { return `(?i)\.git(/|\\),(?i)\.svn(/|\\),(?i)\.env,(?i)database\.(php|xml|json|yaml|yml),(?i)(backup|dump|export)\.(sql|db|tar|zip|gz),(?i)(id_rsa|id_dsa|id_ecdsa|id_ed25519),(?i)\.ssh(/|\\).*,(?i)(access|error|debug)\.log,(?i)web\.config,(?i)phpinfo\.php` } // getSensitivePathsFromConfig 从配置获取敏感路径数组 func getSensitivePathsFromConfig(config model.StaticSiteConfig) []string { if config.SensitivePaths != "" { return splitConfigString(config.SensitivePaths) } return splitConfigString(getDefaultSensitivePathsString()) } // getSensitiveExtensionsFromConfig 从配置获取敏感扩展名数组 func getSensitiveExtensionsFromConfig(config model.StaticSiteConfig) []string { if config.SensitiveExtensions != "" { return splitConfigString(config.SensitiveExtensions) } return splitConfigString(getDefaultSensitiveExtensionsString()) } // getAllowedExtensionsFromConfig 从配置获取允许扩展名数组 func getAllowedExtensionsFromConfig(config model.StaticSiteConfig) []string { if config.AllowedExtensions != "" { return splitConfigString(config.AllowedExtensions) } return splitConfigString(getDefaultAllowedExtensionsString()) } // getSensitivePatternsFromConfig 从配置获取敏感模式数组 func getSensitivePatternsFromConfig(config model.StaticSiteConfig) []string { if config.SensitivePatterns != "" { return splitConfigString(config.SensitivePatterns) } return splitConfigString(getDefaultSensitivePatternsString()) } // serveStaticFile 提供静态文件服务 func (waf *WafEngine) serveStaticFile(w http.ResponseWriter, r *http.Request, config model.StaticSiteConfig, weblog *innerbean.WebLog, hostsafe *wafenginmodel.HostSafe) bool { // 记录访问尝试 startTime := time.Now() defer func() { zlog.Debug("静态文件访问完成", zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), zap.Duration("duration", time.Since(startTime))) }() // 只允许GET和HEAD方法 if r.Method != "GET" && r.Method != "HEAD" { waf.logSecurityEvent("非法HTTP方法", r.URL.Path, r.RemoteAddr, "method: "+r.Method, weblog) w.WriteHeader(http.StatusMethodNotAllowed) return true } // 检查URL长度限制 if len(r.URL.Path) > 1024 { waf.logSecurityEvent("URL过长", r.URL.Path, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusRequestURITooLong) return true } // 提取相对路径(去掉前缀) relativePath := strings.TrimPrefix(r.URL.Path, config.StaticSitePrefix) if relativePath == "" { relativePath = "index.html" // 默认首页 } // URL解码检查 originalPath := relativePath if waf.containsEncodedThreats(relativePath) { waf.logSecurityEvent("检测到编码攻击", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Encoded threat detected")) return true } // 路径穿越攻击检查 if waf.containsPathTraversal(relativePath) { waf.logSecurityEvent("路径穿越攻击", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Path traversal detected")) return true } // 敏感路径检查 if waf.isSensitivePath(relativePath, config) { waf.logSecurityEvent("访问敏感路径", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Access to sensitive path denied")) return true } // 敏感文件检查 if waf.isSensitiveFile(relativePath, config) { waf.logSecurityEvent("访问敏感文件", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Access to sensitive file denied")) return true } // 清理路径 cleanPath := path.Clean(relativePath) // 二次路径穿越检查(清理后) if waf.containsPathTraversal(cleanPath) { waf.logSecurityEvent("清理后仍存在路径穿越", originalPath, r.RemoteAddr, "clean_path: "+cleanPath, weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Path traversal detected after cleaning")) return true } // 构建完整文件路径 fullPath := filepath.Join(config.StaticSitePath, cleanPath) // 验证文件路径是否在允许的目录内 absBasePath, err := filepath.Abs(config.StaticSitePath) if err != nil { zlog.Error("获取基础路径失败", zap.Error(err)) w.WriteHeader(http.StatusInternalServerError) return true } absFullPath, err := filepath.Abs(fullPath) if err != nil { zlog.Error("获取完整路径失败", zap.Error(err)) w.WriteHeader(http.StatusInternalServerError) return true } // 确保请求的文件在静态站点目录内 if !strings.HasPrefix(absFullPath, absBasePath) { waf.logSecurityEvent("目录遍历攻击", originalPath, r.RemoteAddr, "full_path: "+fullPath+", abs_path: "+absFullPath, weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Directory traversal detected")) return true } // 检查文件是否存在和权限 fileInfo, err := os.Stat(absFullPath) if err != nil { if os.IsNotExist(err) { waf.logSecurityEvent("文件未找到", originalPath, r.RemoteAddr, "", weblog) // 记录404但不详细记录路径信息,防止信息泄露 zlog.Info("文件未找到", zap.String("remote_addr", r.RemoteAddr)) w.WriteHeader(http.StatusNotFound) w.Write([]byte("404 Not Found")) } else if os.IsPermission(err) { waf.logSecurityEvent("文件权限拒绝", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Permission denied")) } else { zlog.Error("文件状态检查失败", zap.Error(err)) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("500 Internal Server Error")) } return true } // 不允许访问目录 if fileInfo.IsDir() { waf.logSecurityEvent("尝试访问目录", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: Directory access denied")) return true } // 检查文件类型(基于扩展名) if !waf.isAllowedFileType(absFullPath, config) { waf.logSecurityEvent("不允许的文件类型", originalPath, r.RemoteAddr, "", weblog) w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 Forbidden: File type not allowed")) return true } // 设置安全头 waf.setSecurityHeaders(w) // 记录合法的静态文件访问到日志队列 waf.logStaticFileAccess(r.URL.Path, r.RemoteAddr, fileInfo.Size(), weblog, hostsafe) // 提供文件服务 http.ServeFile(w, r, absFullPath) return true } // containsPathTraversal 检查路径是否包含路径穿越攻击模式(增强版) func (waf *WafEngine) containsPathTraversal(filePath string) bool { // 转换为小写进行检查 lowerPath := strings.ToLower(filePath) // 使用正则表达式检查 for _, pattern := range pathTraversalPatterns { if pattern.MatchString(lowerPath) { return true } } // 额外的字符串检查 suspiciousPatterns := []string{ "../", "..\\", ".../", "...\\", "%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "..%5c", "\\\\", "//", "%252e", "%c0%ae", "%uff0e", } for _, pattern := range suspiciousPatterns { if strings.Contains(lowerPath, pattern) { return true } } return false } // containsEncodedThreats 检查编码攻击 func (waf *WafEngine) containsEncodedThreats(filePath string) bool { lowerPath := strings.ToLower(filePath) // 检查各种编码绕过 encodedThreats := []string{ "%252e", "%c0%ae", "%uff0e", // 双重编码 "%2e%2e%2f", "%2e%2e%5c", // URL编码 "..%c0%af", "..%ef%bc%8f", // UTF-8编码 "..%c1%9c", "..%c1%pc", // 畸形UTF-8 } for _, threat := range encodedThreats { if strings.Contains(lowerPath, threat) { return true } } return false } // isSensitivePath 检查是否为敏感路径 func (waf *WafEngine) isSensitivePath(filePath string, config model.StaticSiteConfig) bool { lowerPath := strings.ToLower(filePath) // 从配置获取敏感路径数组 sensitivePaths := getSensitivePathsFromConfig(config) // 检查敏感路径 for _, sensitivePath := range sensitivePaths { if strings.Contains(lowerPath, strings.ToLower(sensitivePath)) { return true } } return false } // isSensitiveFile 检查是否为敏感文件 func (waf *WafEngine) isSensitiveFile(filePath string, config model.StaticSiteConfig) bool { lowerPath := strings.ToLower(filePath) fileName := filepath.Base(lowerPath) // 从配置获取敏感扩展名数组 sensitiveExtensions := getSensitiveExtensionsFromConfig(config) // 检查敏感文件扩展名 for _, ext := range sensitiveExtensions { if strings.HasSuffix(fileName, ext) { return true } } // 从配置获取敏感模式数组 sensitivePatterns := getSensitivePatternsFromConfig(config) // 检查敏感文件名模式 for _, patternStr := range sensitivePatterns { if pattern, err := regexp.Compile(patternStr); err == nil { if pattern.MatchString(lowerPath) { return true } } } // 检查隐藏文件(以.开头,但不是../或./) if strings.HasPrefix(fileName, ".") && fileName != "." && fileName != ".." { return true } return false } // isAllowedFileType 检查是否为允许的文件类型 func (waf *WafEngine) isAllowedFileType(filePath string, config model.StaticSiteConfig) bool { // 从配置获取允许扩展名数组 allowedExtensions := getAllowedExtensionsFromConfig(config) ext := strings.ToLower(filepath.Ext(filePath)) if ext == "" { return false // 没有扩展名的文件不允许 } for _, allowedExt := range allowedExtensions { if ext == allowedExt { return true } } return false } // setSecurityHeaders 设置安全响应头 func (waf *WafEngine) setSecurityHeaders(w http.ResponseWriter) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("Content-Security-Policy", "default-src 'self'") w.Header().Set("Cache-Control", "public, max-age=3600") } // logStaticFileAccess 记录成功的静态文件访问到传入的weblog func (waf *WafEngine) logStaticFileAccess(path, remoteAddr string, fileSize int64, weblog *innerbean.WebLog, hostsafe *wafenginmodel.HostSafe) { // 更新weblog信息 weblog.ACTION = "放行" weblog.RULE = "静态文件访问成功" weblog.RISK_LEVEL = 0 // 无风险 // 按照全局日志记录策略决定是否记录 if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" { if hostsafe.Host.EXCLUDE_URL_LOG == "" { global.GQEQUE_LOG_DB.Enqueue(weblog) } else { lines := strings.Split(hostsafe.Host.EXCLUDE_URL_LOG, "\n") isRecordLog := true // 检查每一行 for _, line := range lines { if strings.HasPrefix(weblog.URL, line) { isRecordLog = false } } if isRecordLog { global.GQEQUE_LOG_DB.Enqueue(weblog) } } } else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblog.ACTION != "放行" { // 对于静态文件成功访问,ACTION是"放行",所以在abnormal模式下不会记录 // 这里保持逻辑一致性,虽然实际上不会执行 global.GQEQUE_LOG_DB.Enqueue(weblog) } } // logSecurityEvent 记录安全事件 func (waf *WafEngine) logSecurityEvent(eventType, path, remoteAddr, details string, weblog *innerbean.WebLog) { // 更新weblog信息 weblog.ACTION = "禁止" weblog.RULE = "静态文件安全检查: " + eventType weblog.RISK_LEVEL = 3 // 高风险 // 入队到日志系统 global.GQEQUE_LOG_DB.Enqueue(weblog) // 同时记录到系统日志用于调试 zlog.Warn("静态文件安全事件", zap.String("event_type", eventType), zap.String("path", path), zap.String("remote_addr", remoteAddr), zap.String("details", details)) } // 初始化静态站点配置的默认值 func InitDefaultStaticSiteConfig() model.StaticSiteConfig { return model.StaticSiteConfig{ IsEnableStaticSite: 0, StaticSitePath: "", StaticSitePrefix: "/", SensitivePaths: getDefaultSensitivePathsString(), SensitiveExtensions: getDefaultSensitiveExtensionsString(), AllowedExtensions: getDefaultAllowedExtensionsString(), SensitivePatterns: getDefaultSensitivePatternsString(), } }