feat(tools/io): 内置 IO/网络工具(file(路径安全)/http(SSRF防护)/web-search/web-fetch/knowledge)+ 大小截断
This commit is contained in:
1 parent
488cf9c79c
commit
3ce9489f8e
17 files changed
+2223
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrSSRF is returned when a requested URL targets a loopback, private or
|
||||
// link-local address or a localhost hostname that is not on the allowlist.
|
||||
// Use errors.Is to test for it.
|
||||
var ErrSSRF = errors.New("io: disallowed url")
|
||||
|
||||
// validateRequestURL parses rawURL and refuses SSRF-prone targets: it returns an
|
||||
// error wrapping ErrSSRF for loopback/private/link-local/unspecified addresses
|
||||
// and for localhost hostnames, unless the host (or hostname) is on the explicit
|
||||
// allowlist. A hostname that resolves to any denied address is also refused.
|
||||
func validateRequestURL(ctx context.Context, rawURL string, allowed []string) (*url.URL, error) {
|
||||
if strings.TrimSpace(rawURL) == "" {
|
||||
return nil, fmt.Errorf("io: url is required")
|
||||
}
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io: parse url: %w", err)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, fmt.Errorf("io: unsupported scheme %q", u.Scheme)
|
||||
}
|
||||
host := hostOnly(u.Host)
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("io: url has no host")
|
||||
}
|
||||
if isHostAllowed(host, allowed) {
|
||||
return u, nil
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if isDeniedIP(ip) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSSRF, host)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
addrs, err := net.DefaultResolver.LookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io: resolve %q: %w", host, err)
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ip := net.ParseIP(a); ip != nil && isDeniedIP(ip) {
|
||||
return nil, fmt.Errorf("%w: %q resolves to %q", ErrSSRF, host, a)
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// isDeniedIP reports whether ip is a loopback, private, link-local, multicast or
|
||||
// unspecified address — the targets SSRF protection refuses by default.
|
||||
func isDeniedIP(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.IsLoopback() ||
|
||||
ip.IsPrivate() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsUnspecified()
|
||||
}
|
||||
|
||||
// isHostAllowed reports whether the host (with any port stripped) matches an
|
||||
// entry on the allowlist.
|
||||
func isHostAllowed(host string, allowed []string) bool {
|
||||
h := strings.ToLower(hostOnly(host))
|
||||
if h == "" {
|
||||
return false
|
||||
}
|
||||
for _, a := range allowed {
|
||||
if strings.EqualFold(hostOnly(a), h) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hostOnly strips the port (and any IPv6 brackets) from a host:port string.
|
||||
func hostOnly(hostport string) string {
|
||||
h := strings.TrimSpace(hostport)
|
||||
if h == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(h, "[") {
|
||||
if j := strings.Index(h, "]"); j >= 0 {
|
||||
return h[1:j]
|
||||
}
|
||||
return h
|
||||
}
|
||||
if i := strings.LastIndex(h, ":"); i >= 0 {
|
||||
return h[:i]
|
||||
}
|
||||
return h
|
||||
}
|
||||
Reference in new issue
Block a user