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.
cloudreve/pkg/request/ssrftest/ssrf_test.go

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{}))
}