The golem SSRF guard checks a URL's resolved addresses before it connects, as PHP's SSRFGuard does, with the same table the dial guard uses.
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package fetchguard_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/fetchguard"
|
|
)
|
|
|
|
func ExampleFetch() {
|
|
ctx := context.Background()
|
|
// Only the application's image host, at most 5 MiB within 5 seconds.
|
|
images := fetchguard.Policy{
|
|
Mode: fetchguard.AllowHostsMode,
|
|
AllowHosts: []string{"images.example.com"},
|
|
MaxBytes: 5 << 20,
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
// Any public host, for a URL a user pasted.
|
|
public := fetchguard.Policy{Mode: fetchguard.PublicOnlyMode}
|
|
|
|
for _, c := range []struct {
|
|
url string
|
|
policy fetchguard.Policy
|
|
}{
|
|
{"http://images.example.com/cover.jpg", images},
|
|
{"https://cdn.attacker.example/cover.jpg", images},
|
|
{"https://127.0.0.1/admin", public},
|
|
{"https://169.254.169.254/latest/meta-data/", public},
|
|
{"https://[::ffff:10.0.0.1]/", public},
|
|
{"https://%zz", public},
|
|
} {
|
|
// The last argument is the application's config (app.Config), for
|
|
// limits the policy leaves at zero; nil uses the framework defaults.
|
|
_, err := fetchguard.Fetch(ctx, c.url, c.policy, nil)
|
|
var fe *fetchguard.Error
|
|
if errors.As(err, &fe) {
|
|
fmt.Println(fe.Reason, c.url)
|
|
}
|
|
}
|
|
fmt.Println(fetchguard.Defaults())
|
|
// Output:
|
|
// scheme http://images.example.com/cover.jpg
|
|
// invalid_url https://cdn.attacker.example/cover.jpg
|
|
// private_ip https://127.0.0.1/admin
|
|
// private_ip https://169.254.169.254/latest/meta-data/
|
|
// private_ip https://[::ffff:10.0.0.1]/
|
|
// invalid_url https://%zz
|
|
// 10485760 10s
|
|
}
|
|
|
|
// stubVendor stands in for a vendor API in the example; tests use the tide
|
|
// upstream fake the same way.
|
|
type stubVendor struct{}
|
|
|
|
func (stubVendor) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
body := `{"id":42}`
|
|
if r.Header.Get("Authorization") != "Bearer example-token" {
|
|
body = `{"error":"unauthorized"}`
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusCreated,
|
|
Header: http.Header{"Content-Type": {"application/json"}},
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Request: r,
|
|
}, nil
|
|
}
|
|
|
|
func ExampleClient_PostJSON() {
|
|
client, err := fetchguard.NewClient(fetchguard.Policy{
|
|
Mode: fetchguard.AllowHostsMode,
|
|
AllowHosts: []string{"api.example.com"},
|
|
Timeout: 10 * time.Second,
|
|
}, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
header := http.Header{}
|
|
header.Set("Authorization", fetchguard.Bearer("example-token"))
|
|
|
|
// Production code passes its own context; the example routes the call to
|
|
// a stub instead of the network.
|
|
ctx := fetchguard.WithTransport(context.Background(), stubVendor{})
|
|
res, err := client.PostJSON(ctx, "https://api.example.com/v1/items", header, map[string]string{"name": "widget"})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Println(res.StatusCode, string(res.Body))
|
|
// Output:
|
|
// 201 {"id":42}
|
|
}
|
|
|
|
func ExampleIsPrivateAddr() {
|
|
for _, ip := range []string{"8.8.8.8", "10.0.0.7", "::ffff:127.0.0.1"} {
|
|
fmt.Println(ip, fetchguard.IsPrivateAddr(netip.MustParseAddr(ip)))
|
|
}
|
|
// Output:
|
|
// 8.8.8.8 false
|
|
// 10.0.0.7 true
|
|
// ::ffff:127.0.0.1 true
|
|
}
|