You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
5.0 KiB
164 lines
5.0 KiB
// Package ssrftest contains tests for pkg/request SSRF validation. It lives
|
|
// in a separate directory so it can compile even when the parent package's
|
|
// pre-existing _test.go files do not.
|
|
package ssrftest
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestValidateExternalURL_Scheme(t *testing.T) {
|
|
ctx := context.Background()
|
|
cases := []string{
|
|
"file:///etc/passwd",
|
|
"gopher://example.com/_GET",
|
|
"dict://127.0.0.1:11211/stat",
|
|
"ldap://x",
|
|
"javascript:alert(1)",
|
|
"",
|
|
}
|
|
for _, raw := range cases {
|
|
err := request.ValidateExternalURL(ctx, raw, request.SSRFOptions{})
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL, "raw=%q", raw)
|
|
}
|
|
}
|
|
|
|
func TestValidateExternalURL_LocalLiterals(t *testing.T) {
|
|
ctx := context.Background()
|
|
cases := []string{
|
|
"http://localhost/x",
|
|
"http://LOCALHOST:8080/x",
|
|
"http://ip6-localhost/x",
|
|
"http://service.localhost/x",
|
|
"https://localhost.localdomain/x",
|
|
}
|
|
for _, raw := range cases {
|
|
err := request.ValidateExternalURL(ctx, raw, request.SSRFOptions{})
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL, "raw=%q", raw)
|
|
}
|
|
}
|
|
|
|
func TestValidateExternalURL_IPLiterals(t *testing.T) {
|
|
ctx := context.Background()
|
|
cases := []string{
|
|
"http://127.0.0.1:7777/secret",
|
|
"http://[::1]:7780/v6secret",
|
|
"http://10.0.0.5/internal",
|
|
"http://172.16.5.5/internal",
|
|
"http://192.168.1.1/router",
|
|
"http://169.254.169.254/latest/meta",
|
|
"http://169.254.42.42/link-local",
|
|
"http://[fe80::1]/link-local-v6",
|
|
"http://[fc00::1]/ula",
|
|
"http://0.0.0.0/x",
|
|
"http://[::]/x",
|
|
"http://100.64.0.1/cgnat",
|
|
"http://[::ffff:127.0.0.1]/v4mapped",
|
|
"http://[::ffff:10.0.0.1]/v4mappedpriv",
|
|
"http://224.0.0.1/multicast",
|
|
}
|
|
for _, raw := range cases {
|
|
err := request.ValidateExternalURL(ctx, raw, request.SSRFOptions{})
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL, "raw=%q", raw)
|
|
}
|
|
}
|
|
|
|
func TestValidateExternalURL_PublicIP(t *testing.T) {
|
|
ctx := context.Background()
|
|
cases := []string{
|
|
"http://1.1.1.1/",
|
|
"https://8.8.8.8/",
|
|
"http://[2606:4700:4700::1111]/",
|
|
}
|
|
for _, raw := range cases {
|
|
err := request.ValidateExternalURL(ctx, raw, request.SSRFOptions{})
|
|
assert.NoError(t, err, "raw=%q", raw)
|
|
}
|
|
}
|
|
|
|
func TestValidateExternalURL_AllowedHostBypass(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
opt := request.SSRFOptions{AllowedHosts: []string{"localhost"}}
|
|
for _, raw := range []string{
|
|
"http://localhost/file.zip",
|
|
"http://LOCALHOST:8080/file.zip",
|
|
} {
|
|
assert.NoError(t, request.ValidateExternalURL(ctx, raw, opt), "raw=%q", raw)
|
|
}
|
|
|
|
opt = request.SSRFOptions{AllowedHosts: []string{"cloudreve.example.com"}}
|
|
assert.NoError(t, request.ValidateExternalURL(ctx, "https://cloudreve.example.com/x", opt))
|
|
|
|
opt = request.SSRFOptions{AllowedHosts: []string{"127.0.0.1"}}
|
|
assert.NoError(t, request.ValidateExternalURL(ctx, "http://127.0.0.1:7777/secret", opt))
|
|
}
|
|
|
|
func TestValidateExternalURL_Disabled(t *testing.T) {
|
|
ctx := context.Background()
|
|
opt := request.SSRFOptions{Disabled: true}
|
|
|
|
// Everything passes when the operator has explicitly disabled validation.
|
|
for _, raw := range []string{
|
|
"http://127.0.0.1:7777/secret",
|
|
"http://localhost/x",
|
|
"file:///etc/passwd",
|
|
"http://169.254.169.254/latest",
|
|
} {
|
|
assert.NoError(t, request.ValidateExternalURL(ctx, raw, opt), "raw=%q", raw)
|
|
}
|
|
}
|
|
|
|
func TestValidateExternalURL_AllowedCIDR(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// LAN range allowlist lets a NAS at 192.168.10.50 through.
|
|
opt := request.SSRFOptions{AllowedCIDRs: []string{"192.168.10.0/24"}}
|
|
assert.NoError(t, request.ValidateExternalURL(ctx,
|
|
"http://192.168.10.50/share/file.zip", opt))
|
|
|
|
// IPs outside the allowlisted CIDR remain rejected.
|
|
err := request.ValidateExternalURL(ctx,
|
|
"http://192.168.11.50/share/file.zip", opt)
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL)
|
|
|
|
// IPv6 ULA allowlist.
|
|
opt = request.SSRFOptions{AllowedCIDRs: []string{"fd00::/8"}}
|
|
assert.NoError(t, request.ValidateExternalURL(ctx,
|
|
"http://[fd12:3456::1]/x", opt))
|
|
|
|
// Malformed CIDRs are silently dropped without affecting other entries.
|
|
opt = request.SSRFOptions{AllowedCIDRs: []string{"not-a-cidr", "10.0.0.0/8"}}
|
|
assert.NoError(t, request.ValidateExternalURL(ctx, "http://10.5.5.5/x", opt))
|
|
}
|
|
|
|
func TestValidateExternalURL_Magnet(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// DHT-only magnet (no tracker/web seed) is allowed.
|
|
assert.NoError(t, request.ValidateExternalURL(ctx,
|
|
"magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a",
|
|
request.SSRFOptions{}))
|
|
|
|
// Internal tracker URL is rejected.
|
|
err := request.ValidateExternalURL(ctx,
|
|
"magnet:?xt=urn:btih:abc&tr=http://127.0.0.1:7777/announce",
|
|
request.SSRFOptions{})
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL)
|
|
|
|
// Internal web seed is rejected.
|
|
err = request.ValidateExternalURL(ctx,
|
|
"magnet:?xt=urn:btih:abc&ws=http://10.0.0.1/seed",
|
|
request.SSRFOptions{})
|
|
assert.ErrorIs(t, err, request.ErrUnsafeURL)
|
|
|
|
// Public tracker is allowed.
|
|
assert.NoError(t, request.ValidateExternalURL(ctx,
|
|
"magnet:?xt=urn:btih:abc&tr=udp://tracker.opentrackr.org:1337/announce",
|
|
request.SSRFOptions{}))
|
|
}
|