forked from go-openapi/spec
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathexpander_ssrf_test.go
More file actions
79 lines (67 loc) · 2.63 KB
/
Copy pathexpander_ssrf_test.go
File metadata and controls
79 lines (67 loc) · 2.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package spec
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"testing"
"time"
"github.com/go-openapi/swag/loading"
"github.com/go-openapi/testify/v2/require"
)
var errBlockedAddress = errors.New("blocked non-public address")
// restrictedDialContext refuses to dial loopback, private, link-local or unspecified addresses.
// This mirrors the SSRF guard a caller injects via loading.WithHTTPClient (and that
// go-openapi/loads ships as RestrictedHTTPClient).
func restrictedDialContext(_ context.Context, _, addr string) (net.Conn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
ip, err := netip.ParseAddr(host)
if err != nil {
// a hostname would need resolution then a re-check; this test only uses IP literals.
return nil, fmt.Errorf("%w: %s", errBlockedAddress, addr)
}
ip = ip.Unmap()
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
return nil, fmt.Errorf("%w: %s", errBlockedAddress, addr)
}
return nil, fmt.Errorf("%w: %s (test performs no real dial)", errBlockedAddress, addr)
}
// TestExpand_SSRFPosture validates that a caller can neutralize the SSRF vector by injecting an
// option-aware loader bound to a restricted HTTP client through PathLoaderWithOptions: a remote
// "$ref" to a cloud metadata endpoint is refused at dial time, before any connection is made.
//
// The loader selection is shared by every expansion/resolution entry point, so blocking it here
// blocks it for ExpandSpec, ExpandSchemaWithBasePath, ExpandResponse, ExpandParameter and the
// Resolve* functions alike.
func TestExpand_SSRFPosture(t *testing.T) {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: restrictedDialContext},
}
loader := func(pth string, _ ...loading.Option) (json.RawMessage, error) {
b, err := loading.LoadFromFileOrHTTP(pth, loading.WithHTTPClient(client))
return json.RawMessage(b), err
}
// AWS IMDS endpoint, exactly as in the report's PoC
raw := `{
"swagger":"2.0","info":{"title":"x","version":"1"},"paths":{},
"definitions":{
"Victim":{"$ref":"http://169.254.169.254/latest/meta-data/iam/security-credentials/role"}
}
}`
var sw Swagger
require.NoError(t, json.Unmarshal([]byte(raw), &sw))
err := ExpandSpec(&sw, &ExpandOptions{PathLoaderWithOptions: loader})
// the metadata endpoint was refused at dial time: the fetch never happened
require.Error(t, err)
require.ErrorIs(t, err, errBlockedAddress)
require.ErrorContains(t, err, "169.254.169.254")
}