Files
netbird/client/internal/debug/upload_redact_test.go
T
riccardom 9e9f5f304c [client] Strip URLs out of debug bundle upload errors
Redacting the URL in the log line left the error itself untouched, and Go's
*url.Error prints the URL whole. That error does not stay local: it becomes
UploadFailureReason for the CLI and the desktop UI, and for a remote job it is
stored in the management server's job record and shown in the dashboard. A
sample from a failed job:

    Client error: 'upload debug bundle: get presigned URL: Get
    "https://helloworld.asda1234:2356?id=eb23d149..."'

The service URL can carry userinfo or a query token, and the presigned URL the
service hands back carries credentials in its query by design, so the second
step leaks more than the first.

UploadDebugBundle now rewrites every URL in its error down to scheme://host, on
the way out, which covers the daemon, both mobile SDKs and the job runner at
once. The original error stays reachable through Unwrap.

Reported by cubic on #7514.
2026-09-14 11:34:11 +02:00

73 lines
2.0 KiB
Go

package debug
import (
"errors"
"fmt"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRedactURLsInError(t *testing.T) {
sentinel := errors.New("boom")
tests := []struct {
name string
err error
want string
}{
{name: "nil stays nil"},
{
name: "no URL is left alone",
err: errors.New("file too large"),
want: "file too large",
},
{
// What a failed GET actually looks like: *url.Error prints the URL
// whole, query included.
name: "service URL loses its query",
err: fmt.Errorf("get presigned URL: %w", &url.Error{
Op: "Get",
URL: "https://upload.example.com/upload-url?id=deadbeef",
Err: errors.New("no such host"),
}),
want: `get presigned URL: Get "https://upload.example.com": no such host`,
},
{
// The presigned PUT URL is the one that carries credentials.
name: "presigned URL loses its credentials",
err: errors.New(`upload failed: Put "https://bucket.s3.amazonaws.com/k?X-Amz-Signature=abc123&X-Amz-Credential=AKIA": timeout`),
want: `upload failed: Put "https://bucket.s3.amazonaws.com": timeout`,
},
{
name: "userinfo does not survive",
err: errors.New(`Get "https://user:hunter2@upload.example.com/upload-url": refused`),
want: `Get "https://upload.example.com": refused`,
},
{
name: "two URLs are both cut",
err: errors.New(`redirect from https://a.example.com/x?t=1 to https://b.example.com/y?t=2`),
want: `redirect from https://a.example.com to https://b.example.com`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := redactURLsInError(tc.err)
if tc.err == nil {
assert.NoError(t, got)
return
}
require.Error(t, got)
assert.Equal(t, tc.want, got.Error())
})
}
t.Run("the original error stays reachable", func(t *testing.T) {
wrapped := fmt.Errorf(`Get "https://upload.example.com/x?t=1": %w`, sentinel)
assert.ErrorIs(t, redactURLsInError(wrapped), sentinel)
})
}