This commit is contained in:
2025-11-02 20:36:33 +08:00
parent 2773c1a60b
commit 7e2af0e4f5

View File

@@ -1,11 +1,7 @@
package validator
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
@@ -468,12 +464,6 @@ func validateLuhn(cardNumber string) bool {
}
// validateAuthorizationURL 授权书URL验证器
// 验证URL格式、可访问性和文件类型
// 安全措施:
// 1. 仅允许http/https协议
// 2. 设置超时时间防止阻塞
// 3. 限制重定向次数
// 4. 检查Content-Type和文件签名
func validateAuthorizationURL(fl validator.FieldLevel) bool {
urlStr := fl.Field().String()
if urlStr == "" {
@@ -492,7 +482,7 @@ func validateAuthorizationURL(fl validator.FieldLevel) bool {
return false
}
// 检查协议仅允许http和https
// 检查协议
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return false
}
@@ -507,165 +497,8 @@ func validateAuthorizationURL(fl validator.FieldLevel) bool {
break
}
}
if !hasValidExtension {
return false
}
// 验证URL可访问性和文件类型
return validateURLAccessibility(parsedURL)
}
// validateURLAccessibility 验证URL可访问性和文件类型
func validateURLAccessibility(parsedURL *url.URL) bool {
// 创建带超时的context5秒超时避免阻塞验证流程
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 创建HTTP客户端设置安全参数
client := &http.Client{
Timeout: 5 * time.Second,
// 限制重定向次数,防止重定向攻击
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// 最多允许3次重定向
if len(via) >= 3 {
return fmt.Errorf("重定向次数过多")
}
// 检查重定向后的URL是否仍然是http/https
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return fmt.Errorf("不允许重定向到非HTTP协议")
}
return nil
},
}
// 先尝试HEAD请求更高效不下载文件内容
req, err := http.NewRequestWithContext(ctx, "HEAD", parsedURL.String(), nil)
if err != nil {
return false
}
// 设置User-Agent避免某些服务器拒绝请求
req.Header.Set("User-Agent", "TYAPI-Validator/1.0")
// 发送HEAD请求
resp, err := client.Do(req)
if err != nil {
// HEAD请求失败尝试GET请求某些服务器不支持HEAD
return validateWithGETRequest(ctx, client, parsedURL)
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return false
}
// 验证Content-Type
if !isValidContentType(resp.Header.Get("Content-Type")) {
// Content-Type无效尝试读取文件签名验证
return validateWithGETRequest(ctx, client, parsedURL)
}
return true
}
// validateWithGETRequest 使用GET请求验证文件仅在HEAD失败时使用
func validateWithGETRequest(ctx context.Context, client *http.Client, parsedURL *url.URL) bool {
req, err := http.NewRequestWithContext(ctx, "GET", parsedURL.String(), nil)
if err != nil {
return false
}
req.Header.Set("User-Agent", "TYAPI-Validator/1.0")
// 只读取部分内容,不下载整个文件
req.Header.Set("Range", "bytes=0-1023") // 只读取前1024字节
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
// 检查HTTP状态码206是部分内容200是完整内容
if resp.StatusCode != 200 && resp.StatusCode != 206 {
return false
}
// 验证Content-Type
contentType := resp.Header.Get("Content-Type")
if isValidContentType(contentType) {
return true
}
// 读取文件签名magic bytes验证文件类型
return validateFileSignature(resp.Body)
}
// isValidContentType 检查Content-Type是否有效
func isValidContentType(contentType string) bool {
contentType = strings.ToLower(strings.TrimSpace(contentType))
if contentType == "" {
return false
}
// 移除charset等参数只检查主类型
if idx := strings.Index(contentType, ";"); idx != -1 {
contentType = contentType[:idx]
}
contentType = strings.TrimSpace(contentType)
// 允许的Content-Type列表
validContentTypes := []string{
"application/pdf", // PDF
"image/jpeg", // JPEG
"image/jpg", // JPG
"image/png", // PNG
"image/bmp", // BMP
"image/x-ms-bmp", // BMP (另一种MIME类型)
}
for _, validType := range validContentTypes {
if contentType == validType {
return true
}
}
return false
}
// validateFileSignature 通过文件签名magic bytes验证文件类型
func validateFileSignature(body io.Reader) bool {
// 读取文件前16字节足够识别所有支持的文件类型
buffer := make([]byte, 16)
n, err := body.Read(buffer)
if err != nil && err != io.EOF {
return false
}
if n < 4 {
return false
}
// PDF签名: %PDF (前4字节)
if n >= 4 && bytes.Equal(buffer[0:4], []byte("%PDF")) {
return true
}
// JPEG签名: FF D8 FF (前3字节)
if n >= 3 && buffer[0] == 0xFF && buffer[1] == 0xD8 && buffer[2] == 0xFF {
return true
}
// PNG签名: 89 50 4E 47 0D 0A 1A 0A (前8字节)
if n >= 8 && bytes.Equal(buffer[0:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
return true
}
// BMP签名: BM (前2字节: 42 4D)
if n >= 2 && bytes.Equal(buffer[0:2], []byte("BM")) {
return true
}
return false
return hasValidExtension
}
// validateUniqueID 唯一标识验证器小于等于32位字符串