From 4518a7e80c809e7bbbc355eae417a8618852a212 Mon Sep 17 00:00:00 2001 From: Miguel da Costa Martins Marcelino Date: Tue, 22 Sep 2026 19:56:12 +0000 Subject: [PATCH] chore: Avoid logging request body Raw response bodies were being logged by quick tunnel provisioner. This avoids logging the raw body, which could contain sensitive data, while preserving structured Worker errors. --- cmd/cloudflared/tunnel/quick_tunnel.go | 33 +++++++++++---------- cmd/cloudflared/tunnel/quick_tunnel_test.go | 15 ++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/cmd/cloudflared/tunnel/quick_tunnel.go b/cmd/cloudflared/tunnel/quick_tunnel.go index 3e5d7821..8388eedb 100644 --- a/cmd/cloudflared/tunnel/quick_tunnel.go +++ b/cmd/cloudflared/tunnel/quick_tunnel.go @@ -82,26 +82,14 @@ func RunQuickTunnel(sc *subcommandContext) error { } defer func() { _ = resp.Body.Close() }() - // This will read the entire response into memory so we can print it in case of error respBody, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "failed to read quick-tunnel response") } - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - var data QuickTunnelResponse - if err := json.Unmarshal(respBody, &data); err == nil && len(data.Errors) > 0 { - return fmt.Errorf("quick tunnel provisioning failed with status %d: %s", resp.StatusCode, formatQuickTunnelErrors(data.Errors)) - } - return fmt.Errorf("quick tunnel provisioning failed with status %d: %s", resp.StatusCode, string(respBody)) - } - - var data QuickTunnelResponse - if err := json.Unmarshal(respBody, &data); err != nil { - respString := string(respBody) - fields := map[string]interface{}{"status_code": resp.Status} - sc.log.Err(err).Fields(fields).Msgf("Error unmarshaling QuickTunnel response: %s", respString) - return errors.Wrap(err, "failed to unmarshal quick Tunnel") + data, err := decodeQuickTunnelProvisioningResponse(resp.StatusCode, respBody) + if err != nil { + return err } // TODO(TUN-10791): Add CLI-level coverage that provisioning errors are logged to users. @@ -179,6 +167,21 @@ func RunQuickTunnel(sc *subcommandContext) error { ) } +func decodeQuickTunnelProvisioningResponse(statusCode int, response []byte) (QuickTunnelResponse, error) { + var data QuickTunnelResponse + if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { + if err := json.Unmarshal(response, &data); err == nil && len(data.Errors) > 0 { + return QuickTunnelResponse{}, fmt.Errorf("quick tunnel provisioning failed with status %d: %s", statusCode, formatQuickTunnelErrors(data.Errors)) + } + return QuickTunnelResponse{}, fmt.Errorf("quick tunnel provisioning failed with status %d", statusCode) + } + + if err := json.Unmarshal(response, &data); err != nil { + return QuickTunnelResponse{}, errors.Wrap(err, "failed to unmarshal quick Tunnel") + } + return data, nil +} + type QuickTunnelResponse struct { Success bool Result QuickTunnel diff --git a/cmd/cloudflared/tunnel/quick_tunnel_test.go b/cmd/cloudflared/tunnel/quick_tunnel_test.go index 9aa10860..ccf5c40d 100644 --- a/cmd/cloudflared/tunnel/quick_tunnel_test.go +++ b/cmd/cloudflared/tunnel/quick_tunnel_test.go @@ -2,6 +2,7 @@ package tunnel import ( "encoding/json" + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -27,6 +28,20 @@ func TestBuildQuickTunnelRequestBody_ProtectedMode(t *testing.T) { assert.Equal(t, quickTunnelAuthModeOTP, result[quickTunnelAuthModeField]) } +func TestDecodeQuickTunnelProvisioningResponseDoesNotExposeBody(t *testing.T) { + t.Parallel() + + const credential = "sensitive-tunnel-credential" + malformedSuccess := []byte(`{"success":true,"result":{"secret":"` + credential + `"}} trailing`) + _, err := decodeQuickTunnelProvisioningResponse(http.StatusOK, malformedSuccess) + require.Error(t, err) + assert.NotContains(t, err.Error(), credential) + + _, err = decodeQuickTunnelProvisioningResponse(http.StatusBadGateway, []byte(credential)) + require.Error(t, err) + assert.NotContains(t, err.Error(), credential) +} + func TestFormatQuickTunnelErrors(t *testing.T) { t.Parallel()