防御一些爬虫陷阱
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user