fix
This commit is contained in:
@@ -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 {
|
||||
// 创建带超时的context(5秒超时,避免阻塞验证流程)
|
||||
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位字符串)
|
||||
|
||||
Reference in New Issue
Block a user