防御一些爬虫陷阱

This commit is contained in:
2026-04-09 17:03:10 +08:00
parent 2ab89b39db
commit 3715b03fab
3 changed files with 206 additions and 12 deletions
+75
View File
@@ -6,6 +6,7 @@ package crawler
import (
"fmt" // 字符串格式化(构建 robots.txt URL、错误信息)
"io" // IO 接口(读取响应体)
"net" // IP 地址解析(SSRF 防护)
"net/http" // HTTP 客户端
"net/url" // URL 解析
"strings" // 字符串操作
@@ -120,6 +121,10 @@ func (f *Fetcher) fetchWithHistory(rawURL string, polite bool, timeout time.Dura
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
// SSRF 防护:拒绝重定向到内网 IP 或非标端口
if err := isSafeRedirect(req.URL); err != nil {
return err
}
// 记录永久重定向
if req.Response != nil && (req.Response.StatusCode == 301 || req.Response.StatusCode == 308) {
from := via[len(via)-1].URL.String()
@@ -130,6 +135,11 @@ func (f *Fetcher) fetchWithHistory(rawURL string, polite bool, timeout time.Dura
},
}
// 对初始 URL 也做 SSRF 检查
if err := isSafeRedirect(parsed); err != nil {
return nil, &ErrCrawl{Msg: err.Error()}
}
// 构造 GET 请求
req, _ := http.NewRequest("GET", rawURL, nil)
req.Header.Set("User-Agent", f.userAgent)
@@ -344,3 +354,68 @@ func decodeBody(r io.Reader, contentType string, sizeLimit int) (string, error)
}
return string(data), nil
}
// isPrivateIP 检查 IP 是否为私有/回环/链路本地地址。
func isPrivateIP(ip net.IP) bool {
privateRanges := []struct {
network *net.IPNet
}{
// 10.0.0.0/8 — RFC 1918 私有网络
{mustParseCIDR("10.0.0.0/8")},
// 172.16.0.0/12 — RFC 1918 私有网络
{mustParseCIDR("172.16.0.0/12")},
// 192.168.0.0/16 — RFC 1918 私有网络
{mustParseCIDR("192.168.0.0/16")},
// 127.0.0.0/8 — 回环地址
{mustParseCIDR("127.0.0.0/8")},
// 169.254.0.0/16 — 链路本地(AWS/GCP 元数据服务)
{mustParseCIDR("169.254.0.0/16")},
// ::1/128 — IPv6 回环
{mustParseCIDR("::1/128")},
// fe80::/10 — IPv6 链路本地
{mustParseCIDR("fe80::/10")},
// fc00::/7 — IPv6 唯一本地地址
{mustParseCIDR("fc00::/7")},
}
for _, r := range privateRanges {
if r.network.Contains(ip) {
return true
}
}
return false
}
// mustParseCIDR 解析 CIDR,失败时 panic(仅用于编译期常量)。
func mustParseCIDR(s string) *net.IPNet {
_, network, err := net.ParseCIDR(s)
if err != nil {
panic("invalid CIDR: " + s)
}
return network
}
// isSafeRedirect 检查重定向目标是否安全(非内网 IP、非非标端口)。
// 用于防止 SSRF 攻击:恶意服务器将爬虫重定向到内网服务。
func isSafeRedirect(u *url.URL) error {
host := u.Hostname()
port := u.Port()
// 解析 IP 地址
ip := net.ParseIP(host)
if ip == nil {
// 域名(非 IP),允许(DNS 解析由系统处理)
// 但非标端口仍需检查
if port != "" && port != "80" && port != "443" {
return fmt.Errorf("blocked: non-standard port %s", port)
}
return nil
}
// IP 直连:检查是否为私有地址
if isPrivateIP(ip) {
return fmt.Errorf("blocked: private IP %s", ip)
}
// 非标端口检查
if port != "" && port != "80" && port != "443" {
return fmt.Errorf("blocked: non-standard port %s", port)
}
return nil
}