Skip to content

Commit 3c5115b

Browse files
authored
Merge pull request from GHSA-q7gm-mjrg-44h9
Enforce Strict ID Shape v0.12
2 parents c66920f + 056cad7 commit 3c5115b

36 files changed

Lines changed: 854 additions & 73 deletions

File tree

cmd/spire-server/cli/run/run.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ type serverConfig struct {
8787
ProfilingFreq int `hcl:"profiling_freq"`
8888
ProfilingNames []string `hcl:"profiling_names"`
8989

90+
AllowUnsafeIDs *bool `hcl:"allow_unsafe_ids"`
91+
9092
UnusedKeys []string `hcl:",unusedKeys"`
9193
}
9294

@@ -471,6 +473,12 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool
471473
}
472474
}
473475

476+
// This is a terrible hack but is just a short-term band-aid.
477+
if c.Server.AllowUnsafeIDs != nil {
478+
sc.Log.Warn("The insecure allow_unsafe_ids configurable will be deprecated in a future release.")
479+
idutil.SetAllowUnsafeIDs(*c.Server.AllowUnsafeIDs)
480+
}
481+
474482
return sc, nil
475483
}
476484

pkg/common/idutil/safety.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package idutil
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
"path"
8+
"regexp"
9+
"strings"
10+
11+
"github.com/spiffe/go-spiffe/v2/spiffeid"
12+
"github.com/spiffe/spire/proto/spire/types"
13+
)
14+
15+
var (
16+
rePercentEncodedASCII = regexp.MustCompile(`%[0-7][[:xdigit:]]`)
17+
rePercentEncoded = regexp.MustCompile(`%[[:xdigit:]][[:xdigit:]]`)
18+
19+
allowUnsafeIDsPolicy bool
20+
)
21+
22+
func allowUnsafeIDs() bool {
23+
return allowUnsafeIDsPolicy
24+
}
25+
26+
// SetAllowUnsafeIDs effectively removes all safety checks provided by the
27+
// "safety" functions in this source file. It is a switch to allow turning off
28+
// the safety valve for deployments that need time to adjust API usage to
29+
// conform to the restrictions.
30+
func SetAllowUnsafeIDs(allow bool) {
31+
allowUnsafeIDsPolicy = allow
32+
}
33+
34+
// CheckIDProtoNormalization ensures the the provided ID is properly normalized.
35+
func CheckIDProtoNormalization(in *types.SPIFFEID) error {
36+
if allowUnsafeIDs() {
37+
return nil
38+
}
39+
s, err := IDProtoString(in)
40+
if err != nil {
41+
return err
42+
}
43+
return CheckIDStringNormalization(s)
44+
}
45+
46+
// CheckIDStringNormalization ensures the the provided ID is properly normalized.
47+
func CheckIDStringNormalization(id string) error {
48+
if allowUnsafeIDs() {
49+
return nil
50+
}
51+
52+
// Parse the URL. This will unescape the percent-encoded characters. If
53+
// there are invalid percent-encoded characters, this function will fail.
54+
u, err := url.Parse(id)
55+
if err != nil {
56+
return err
57+
}
58+
59+
return CheckIDURLNormalization(u)
60+
}
61+
62+
// CheckIDURLNormalization returns if a URL is normalized or not. It relies on
63+
// behavior and fields populated by url.Parse(). DO NOT call it with a URL that
64+
// has not gone through url.Parse().
65+
func CheckIDURLNormalization(u *url.URL) error {
66+
if allowUnsafeIDs() {
67+
return nil
68+
}
69+
70+
// Rule out percent-encoded ASCII
71+
if rePercentEncodedASCII.MatchString(u.EscapedPath()) {
72+
return errors.New("path cannot contain percent-encoded ASCII characters")
73+
}
74+
75+
// At this point, if RawPath is set, then the path contains non-ASCII
76+
// characters, since percent-encoded ASCII was ruled out above. Ensure
77+
// that there is no percent-encoded characters, since that would imply
78+
// a mix-n-match of utf-8 and percent-encoded utf-8, which we want to
79+
// reject since it wouldn't be normal to have this kind of path and it
80+
// likely indicates either 1) a bug, or 2) malicious intent.
81+
if u.RawPath != "" && rePercentEncoded.MatchString(u.RawPath) {
82+
return errors.New("path cannot contain both non-ASCII and percent-encoded characters")
83+
}
84+
85+
// Check the scheme and host
86+
switch {
87+
case u.Scheme != "spiffe":
88+
return errors.New("scheme must be 'spiffe'")
89+
case strings.ToLower(u.Host) != u.Host:
90+
return errors.New("trust domain name must be lowercase")
91+
}
92+
93+
// Check the path
94+
switch {
95+
case u.Path == "":
96+
case u.Path[len(u.Path)-1] == '/':
97+
return errors.New("path cannot have a trailing slash")
98+
case u.Path != path.Clean(u.Path):
99+
return errors.New("path cannot contain empty, '.', or '..' segments")
100+
}
101+
102+
return nil
103+
}
104+
105+
// IDProtoString constructs a URL string for the given ID protobuf. It does
106+
// not interpret the contents of the trust domain or path with the exception
107+
// of adding a leading slash on the path where necessary.
108+
func IDProtoString(id *types.SPIFFEID) (string, error) {
109+
if id.TrustDomain == "" {
110+
return "", errors.New("trust domain is empty")
111+
}
112+
return "spiffe://" + id.TrustDomain + ensureLeadingSlash(id.Path), nil
113+
}
114+
115+
// IDProtoFromString parses a SPIFFE ID string into the raw ID proto components.
116+
// It does not attempt to escape/unescape any portion of the ID.
117+
func IDProtoFromString(id string) (*types.SPIFFEID, error) {
118+
trimmed := strings.TrimPrefix(id, "spiffe://")
119+
if trimmed == id {
120+
return nil, errors.New(`scheme must be "spiffe://"`)
121+
}
122+
parts := strings.SplitN(trimmed, "/", 2)
123+
td := parts[0]
124+
if len(td) == 0 {
125+
return nil, errors.New("trust domain is empty")
126+
}
127+
path := ""
128+
if len(parts) > 1 {
129+
path = "/" + parts[1]
130+
}
131+
return &types.SPIFFEID{
132+
TrustDomain: td,
133+
Path: path,
134+
}, nil
135+
}
136+
137+
// CheckAgentIDStringNormalization ensures the provided agent ID string is
138+
// properly normalized. It also ensures it is not a server ID.
139+
func CheckAgentIDStringNormalization(agentID string) error {
140+
if allowUnsafeIDs() {
141+
return nil
142+
}
143+
144+
// Parse the URL. This will unescape the percent-encoded characters. If
145+
// there are invalid percent-encoded characters, this function will fail.
146+
u, err := url.Parse(agentID)
147+
if err != nil {
148+
return err
149+
}
150+
151+
if err := CheckIDURLNormalization(u); err != nil {
152+
return err
153+
}
154+
155+
// We want to do more than this but backcompat compels us to not too. We'll
156+
// get more aggressive in the future.
157+
if u.Path == ServerIDPath {
158+
return errors.New("server ID is not allowed for agents")
159+
}
160+
161+
return nil
162+
}
163+
164+
// IDFromProto returns SPIFFE ID from the proto representation
165+
func IDFromProto(id *types.SPIFFEID) (spiffeid.ID, error) {
166+
if allowUnsafeIDs() {
167+
return spiffeid.New(id.TrustDomain, id.Path)
168+
}
169+
s, err := IDProtoString(id)
170+
if err != nil {
171+
return spiffeid.ID{}, err
172+
}
173+
return spiffeid.FromString(s)
174+
}
175+
176+
// FormatPath formats a path string. The function ensures a leading slash is
177+
// present.
178+
func FormatPath(format string, args ...interface{}) string {
179+
return ensureLeadingSlash(fmt.Sprintf(format, args...))
180+
}
181+
182+
// JoinPathSegments escapes path segments and joins them together. The
183+
// function also ensures a leading slash is present.
184+
func JoinPathSegments(segments ...string) string {
185+
return ensureLeadingSlash(strings.Join(segments, "/"))
186+
}
187+
188+
func ensureLeadingSlash(p string) string {
189+
if p != "" && p[0] != '/' {
190+
p = "/" + p
191+
}
192+
return p
193+
}

0 commit comments

Comments
 (0)