Skip to content

Commit 53e6bea

Browse files
authored
Redact sensitive information in redirected URLs (#1408)
* Redact sensitive information in redirected URLs This adds internal methods to redact potentially sensitive information from URLs in error messages, especially when those URLs are the result of server-side redirects. We already redact potentially sensitive information like this as part of transport.CheckError, but this isn't called when the error is the result of an http.Client.Do (e.g., tcp dial error). The specific use case where this can happen is a registry like GCR which redirects blob requests to GCS with a sensitive access_token in the query parameter. If the request to GCS fails due to tcp error, the error message will include the sensitive access token. This method of redaction relies on the original error being a *url.Error, and redaction is accomplished by simply updating the error's URL with a redacted equivalent. * review feedback
1 parent d187a71 commit 53e6bea

File tree

4 files changed

+102
-38
lines changed

4 files changed

+102
-38
lines changed

Diff for: internal/redact/redact.go

+54
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package redact
1717

1818
import (
1919
"context"
20+
"errors"
21+
"net/url"
2022
)
2123

2224
type contextKey string
@@ -33,3 +35,55 @@ func FromContext(ctx context.Context) (bool, string) {
3335
reason, ok := ctx.Value(redactKey).(string)
3436
return ok, reason
3537
}
38+
39+
// Error redacts potentially sensitive query parameter values in the URL from the error's message.
40+
//
41+
// If the error is a *url.Error, this returns a *url.Error with the URL redacted.
42+
// Any other error type, or nil, is returned unchanged.
43+
func Error(err error) error {
44+
// If the error is a url.Error, we can redact the URL.
45+
// Otherwise (including if err is nil), we can't redact.
46+
var uerr *url.Error
47+
if ok := errors.As(err, &uerr); !ok {
48+
return err
49+
}
50+
u, perr := url.Parse(uerr.URL)
51+
if perr != nil {
52+
return err // If the URL can't be parsed, just return the original error.
53+
}
54+
uerr.URL = URL(u).String() // Update the URL to the redacted URL.
55+
return uerr
56+
}
57+
58+
// The set of query string keys that we expect to send as part of the registry
59+
// protocol. Anything else is potentially dangerous to leak, as it's probably
60+
// from a redirect. These redirects often included tokens or signed URLs.
61+
var paramAllowlist = map[string]struct{}{
62+
// Token exchange
63+
"scope": {},
64+
"service": {},
65+
// Cross-repo mounting
66+
"mount": {},
67+
"from": {},
68+
// Layer PUT
69+
"digest": {},
70+
// Listing tags and catalog
71+
"n": {},
72+
"last": {},
73+
}
74+
75+
// URL redacts potentially sensitive query parameter values from the URL's query string.
76+
func URL(u *url.URL) *url.URL {
77+
qs := u.Query()
78+
for k, v := range qs {
79+
for i := range v {
80+
if _, ok := paramAllowlist[k]; !ok {
81+
// key is not in the Allowlist
82+
v[i] = "REDACTED"
83+
}
84+
}
85+
}
86+
r := *u
87+
r.RawQuery = qs.Encode()
88+
return &r
89+
}

Diff for: pkg/v1/remote/descriptor.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net/url"
2525
"strings"
2626

27+
"github.com/google/go-containerregistry/internal/redact"
2728
"github.com/google/go-containerregistry/internal/verify"
2829
"github.com/google/go-containerregistry/pkg/logs"
2930
"github.com/google/go-containerregistry/pkg/name"
@@ -367,7 +368,7 @@ func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.Read
367368

368369
resp, err := f.Client.Do(req.WithContext(ctx))
369370
if err != nil {
370-
return nil, err
371+
return nil, redact.Error(err)
371372
}
372373

373374
if err := transport.CheckError(resp, http.StatusOK); err != nil {
@@ -398,7 +399,7 @@ func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
398399

399400
resp, err := f.Client.Do(req.WithContext(f.context))
400401
if err != nil {
401-
return nil, err
402+
return nil, redact.Error(err)
402403
}
403404

404405
if err := transport.CheckError(resp, http.StatusOK); err != nil {
@@ -418,7 +419,7 @@ func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
418419

419420
resp, err := f.Client.Do(req.WithContext(f.context))
420421
if err != nil {
421-
return false, err
422+
return false, redact.Error(err)
422423
}
423424
defer resp.Body.Close()
424425

Diff for: pkg/v1/remote/descriptor_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package remote
1616

1717
import (
18+
"context"
1819
"errors"
1920
"fmt"
2021
"net/http"
@@ -25,6 +26,7 @@ import (
2526
"testing"
2627

2728
"github.com/google/go-cmp/cmp"
29+
v1 "github.com/google/go-containerregistry/pkg/v1"
2830
"github.com/google/go-containerregistry/pkg/v1/types"
2931
)
3032

@@ -216,3 +218,42 @@ func TestHead_MissingHeaders(t *testing.T) {
216218
}
217219
}
218220
}
221+
222+
// TestRedactFetchBlob tests that a request to fetchBlob that gets redirected
223+
// to a URL that contains sensitive information has that information redacted
224+
// if the subsequent request fails.
225+
func TestRedactFetchBlob(t *testing.T) {
226+
ctx := context.Background()
227+
f := fetcher{
228+
Ref: mustNewTag(t, "original.com/repo:latest"),
229+
Client: &http.Client{
230+
Transport: errTransport{},
231+
},
232+
context: ctx,
233+
}
234+
h, err := v1.NewHash("sha256:0000000000000000000000000000000000000000000000000000000000000000")
235+
if err != nil {
236+
t.Fatal("NewHash:", err)
237+
}
238+
if _, err := f.fetchBlob(ctx, 0, h); err == nil {
239+
t.Fatalf("fetchBlob: expected error, got nil")
240+
} else if !strings.Contains(err.Error(), "access_token=REDACTED") {
241+
t.Fatalf("fetchBlob: expected error to contain redacted access token, got %v", err)
242+
}
243+
}
244+
245+
type errTransport struct{}
246+
247+
func (errTransport) RoundTrip(req *http.Request) (*http.Response, error) {
248+
// This simulates a registry that returns a redirect upon the first
249+
// request, and then returns an error upon subsequent requests. This helps
250+
// test whether error redaction takes into account URLs in error messasges
251+
// that are not the original request URL.
252+
if req.URL.Host == "original.com" {
253+
return &http.Response{
254+
StatusCode: http.StatusSeeOther,
255+
Header: http.Header{"Location": []string{"https://redirected.com?access_token=SECRET"}},
256+
}, nil
257+
}
258+
return nil, fmt.Errorf("error reaching %s", req.URL.String())
259+
}

Diff for: pkg/v1/remote/transport/error.go

+3-35
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,10 @@ import (
1919
"fmt"
2020
"io/ioutil"
2121
"net/http"
22-
"net/url"
2322
"strings"
24-
)
2523

26-
// The set of query string keys that we expect to send as part of the registry
27-
// protocol. Anything else is potentially dangerous to leak, as it's probably
28-
// from a redirect. These redirects often included tokens or signed URLs.
29-
var paramAllowlist = map[string]struct{}{
30-
// Token exchange
31-
"scope": {},
32-
"service": {},
33-
// Cross-repo mounting
34-
"mount": {},
35-
"from": {},
36-
// Layer PUT
37-
"digest": {},
38-
// Listing tags and catalog
39-
"n": {},
40-
"last": {},
41-
}
24+
"github.com/google/go-containerregistry/internal/redact"
25+
)
4226

4327
// Error implements error to support the following error specification:
4428
// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
@@ -59,7 +43,7 @@ var _ error = (*Error)(nil)
5943
func (e *Error) Error() string {
6044
prefix := ""
6145
if e.Request != nil {
62-
prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redactURL(e.Request.URL))
46+
prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redact.URL(e.Request.URL))
6347
}
6448
return prefix + e.responseErr()
6549
}
@@ -100,22 +84,6 @@ func (e *Error) Temporary() bool {
10084
return true
10185
}
10286

103-
// TODO(jonjohnsonjr): Consider moving to internal/redact.
104-
func redactURL(original *url.URL) *url.URL {
105-
qs := original.Query()
106-
for k, v := range qs {
107-
for i := range v {
108-
if _, ok := paramAllowlist[k]; !ok {
109-
// key is not in the Allowlist
110-
v[i] = "REDACTED"
111-
}
112-
}
113-
}
114-
redacted := *original
115-
redacted.RawQuery = qs.Encode()
116-
return &redacted
117-
}
118-
11987
// Diagnostic represents a single error returned by a Docker registry interaction.
12088
type Diagnostic struct {
12189
Code ErrorCode `json:"code"`

0 commit comments

Comments
 (0)