Ctrl K
ComunidadGuías…

http/token_header_size_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package http

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"

	cleanhttp "github.com/hashicorp/go-cleanhttp"
	"github.com/hashicorp/vault/internalshared/configutil"
	"github.com/hashicorp/vault/sdk/helper/consts"
	"github.com/hashicorp/vault/vault"
	"github.com/stretchr/testify/require"
)

// sendTokenHeaderRequest builds and dispatches an HTTP GET to addr, placing
// token in the named header. For "Authorization", the value is wrapped as a
// Bearer token per RFC 6750.
func sendTokenHeaderRequest(t *testing.T, client *http.Client, addr, headerName, token string) (*http.Response, error) {
	t.Helper()
	req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil)
	require.NoError(t, err)
	if headerName == "Authorization" {
		req.Header.Set("Authorization", "Bearer "+token)
	} else {
		req.Header.Set(headerName, token)
	}
	return client.Do(req)
}

// newTestClusterForTokenHeader creates a NewTestCluster with default settings
// and returns the cluster, an HTTP client configured with TLS, and the address.
func newTestClusterForTokenHeader(t *testing.T, opts *vault.TestClusterOptions) (*http.Client, string) {
	t.Helper()
	if opts == nil {
		opts = &vault.TestClusterOptions{}
	}
	opts.HandlerFunc = Handler
	cluster := vault.NewTestCluster(t, nil, opts)

	core := cluster.Cores[0]
	transport := cleanhttp.DefaultPooledTransport()
	transport.TLSClientConfig = core.TLSConfig()
	httpClient := &http.Client{Transport: transport, Timeout: 15 * time.Second}
	return httpClient, core.Client.Address()
}

// TestTokenHeader_ExceedsDefaultLimit_IsRejected verifies that tokens larger than
// DefaultMaxTokenHeaderSize are rejected with 431 before token validation occurs.
func TestTokenHeader_ExceedsDefaultLimit_IsRejected(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, nil)

	cases := []struct {
		name       string
		headerSize int
		headerName string
	}{
		{
			name:       "just-over-limit-x-vault-token",
			headerSize: DefaultMaxTokenHeaderSize + 1,
			headerName: consts.AuthHeaderName,
		},
		{
			name:       "just-over-limit-authorization-bearer",
			headerSize: DefaultMaxTokenHeaderSize + 1,
			headerName: "Authorization",
		},
		{
			name:       "100kb-x-vault-token",
			headerSize: 100 * 1024,
			headerName: consts.AuthHeaderName,
		},
		{
			name:       "100kb-authorization-bearer",
			headerSize: 100 * 1024,
			headerName: "Authorization",
		},
		{
			name:       "900kb-x-vault-token",
			headerSize: 900 * 1024,
			headerName: consts.AuthHeaderName,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			token := strings.Repeat("x", tc.headerSize)
			resp, err := sendTokenHeaderRequest(t, client, addr, tc.headerName, token)
			require.NoError(t, err)
			defer resp.Body.Close()

			require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode,
				"token of %d bytes must be rejected by Vault middleware (want 431, not 403)",
				tc.headerSize)
		})
	}
}

// TestTokenHeader_WithinDefaultLimit_IsProcessed verifies that tokens at or
// below DefaultMaxTokenHeaderSize reach token validation normally (403, not 400).
func TestTokenHeader_WithinDefaultLimit_IsProcessed(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, nil)

	cases := []struct {
		name       string
		headerSize int
		headerName string
	}{
		{
			name:       "512b-x-vault-token",
			headerSize: 512,
			headerName: consts.AuthHeaderName,
		},
		{
			name:       "512b-authorization-bearer",
			headerSize: 512,
			headerName: "Authorization",
		},
		{
			name:       "4kb-x-vault-token",
			headerSize: 4 * 1024,
			headerName: consts.AuthHeaderName,
		},
		{
			name:       "at-limit-x-vault-token",
			headerSize: DefaultMaxTokenHeaderSize,
			headerName: consts.AuthHeaderName,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			token := strings.Repeat("x", tc.headerSize)
			resp, err := sendTokenHeaderRequest(t, client, addr, tc.headerName, token)
			require.NoError(t, err)
			defer resp.Body.Close()

			require.Equal(t, http.StatusForbidden, resp.StatusCode,
				"token of %d bytes is within the limit and must reach validation", tc.headerSize)
		})
	}
}

// TestTokenHeader_ConfigurableLimit_Enforced verifies that an operator can
// lower the token header size limit via the listener configuration, and that
// Vault enforces the configured value rather than DefaultMaxTokenHeaderSize.
func TestTokenHeader_ConfigurableLimit_Enforced(t *testing.T) {
	t.Parallel()

	const customLimit = 1024 // 1 KB — well below the default 8 KB
	client, addr := newTestClusterForTokenHeader(t, &vault.TestClusterOptions{
		DefaultHandlerProperties: vault.HandlerProperties{
			ListenerConfig: &configutil.Listener{
				CustomMaxTokenHeaderSize: customLimit,
			},
		},
	})

	oversized := strings.Repeat("x", customLimit+1)
	resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, oversized)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode,
		"token exceeding custom limit of %d bytes must be rejected with 431", customLimit)

	atLimit := strings.Repeat("x", customLimit)
	resp2, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, atLimit)
	require.NoError(t, err)
	defer resp2.Body.Close()

	require.Equal(t, http.StatusForbidden, resp2.StatusCode,
		"token at the custom limit (%d bytes) must reach validation", customLimit)
}

// TestTokenHeader_MultipleAuthorizationHeaders_BypassPrevented verifies that an
// oversized Bearer token is rejected even when a non-Bearer Authorization header
// precedes it in the request.
func TestTokenHeader_MultipleAuthorizationHeaders_BypassPrevented(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, nil)

	oversizedBearer := strings.Repeat("x", DefaultMaxTokenHeaderSize+1)

	req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil)
	require.NoError(t, err)
	req.Header.Add("Authorization", "Basic dXNlcjpwYXNz")
	req.Header.Add("Authorization", "Bearer "+oversizedBearer)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode,
		"oversized Bearer token must be rejected even when a non-Bearer Authorization header precedes it")
}

// TestTokenHeader_DisabledLimit_AllowsOversizedToken verifies that setting
// max_token_header_size = -1 in the listener config fully disables the check,
// allowing tokens larger than DefaultMaxTokenHeaderSize to reach validation.
func TestTokenHeader_DisabledLimit_AllowsOversizedToken(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, &vault.TestClusterOptions{
		DefaultHandlerProperties: vault.HandlerProperties{
			ListenerConfig: &configutil.Listener{
				CustomMaxTokenHeaderSize: -1,
			},
		},
	})

	oversized := strings.Repeat("x", DefaultMaxTokenHeaderSize*2)
	resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, oversized)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusForbidden, resp.StatusCode,
		"with max_token_header_size = -1 the size guard must not fire; token must reach validation")
}

// TestTokenHeader_ErrorResponseFormat verifies that oversized-token rejections
// return a valid Vault JSON error envelope with an "errors" array.
func TestTokenHeader_ErrorResponseFormat(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, nil)

	token := strings.Repeat("x", DefaultMaxTokenHeaderSize+1)
	resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, token)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode)
	require.Equal(t, "application/json", resp.Header.Get("Content-Type"))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	var envelope struct {
		Errors []string `json:"errors"`
	}
	require.NoError(t, json.Unmarshal(body, &envelope),
		"response body must be valid JSON: %s", string(body))
	require.NotEmpty(t, envelope.Errors,
		"response must contain at least one error message")
	require.Contains(t, envelope.Errors[0], "authentication token",
		"error message must describe the token size violation")
}

// TestTokenHeader_NoAuthHeader_Unaffected verifies that requests with no
// authentication header pass through wrapTokenHeaderSizeHandler unchanged.
func TestTokenHeader_NoAuthHeader_Unaffected(t *testing.T) {
	t.Parallel()

	client, addr := newTestClusterForTokenHeader(t, nil)

	req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil)
	require.NoError(t, err)

	resp, err := client.Do(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusForbidden, resp.StatusCode,
		"requests with no auth header must not be rejected by the size guard")
}

// BenchmarkTokenHeader_ProcessingCost measures the per-request overhead of
// wrapTokenHeaderSizeHandler at increasing token sizes.
func BenchmarkTokenHeader_ProcessingCost(b *testing.B) {
	core, _, _ := vault.TestCoreUnsealed(b)
	ln, addr := TestServer(b, core)
	defer ln.Close()

	sizes := []struct {
		label string
		bytes int
	}{
		{"1KB", 1 * 1024},
		{"8KB", 8 * 1024},   // at default limit
		{"64KB", 64 * 1024}, // above default limit — fast-rejected by wrapTokenHeaderSizeHandler
		{"512KB", 512 * 1024},
	}

	client := cleanhttp.DefaultClient()
	client.Timeout = 30 * time.Second

	for _, sz := range sizes {
		token := strings.Repeat("x", sz.bytes)
		b.Run(fmt.Sprintf("size=%s", sz.label), func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				req, _ := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil)
				req.Header.Set(consts.AuthHeaderName, token)
				resp, err := client.Do(req)
				if err == nil {
					io.Copy(io.Discard, resp.Body)
					resp.Body.Close()
				}
			}
		})
	}
}

// TestTokenHeaderMaxBytes_NilConfig verifies that a nil listener config
// returns DefaultMaxTokenHeaderSize.
func TestTokenHeaderMaxBytes_NilConfig(t *testing.T) {
	t.Parallel()
	require.Equal(t, DefaultMaxTokenHeaderSize, TokenHeaderMaxBytes(nil))
}

// TestTokenHeaderMaxBytes_ZeroCustomSize verifies that a zero CustomMaxTokenHeaderSize
// falls back to DefaultMaxTokenHeaderSize.
func TestTokenHeaderMaxBytes_ZeroCustomSize(t *testing.T) {
	t.Parallel()
	lnConfig := &configutil.Listener{} // CustomMaxTokenHeaderSize == 0
	require.Equal(t, DefaultMaxTokenHeaderSize, TokenHeaderMaxBytes(lnConfig))
}

// TestTokenHeaderMaxBytes_CustomSize verifies that a positive CustomMaxTokenHeaderSize
// is returned verbatim.
func TestTokenHeaderMaxBytes_CustomSize(t *testing.T) {
	t.Parallel()
	lnConfig := &configutil.Listener{CustomMaxTokenHeaderSize: 4096}
	require.Equal(t, 4096, TokenHeaderMaxBytes(lnConfig))
}

// TestTokenHeaderMaxBytes_Disabled verifies that max_token_header_size = -1
// returns 0, leaving http.Server.MaxHeaderBytes at the Go stdlib default.
func TestTokenHeaderMaxBytes_Disabled(t *testing.T) {
	t.Parallel()
	lnConfig := &configutil.Listener{CustomMaxTokenHeaderSize: -1}
	require.Equal(t, 0, TokenHeaderMaxBytes(lnConfig))
}

// TestTokenHeader_StdlibBackstop_NonAuthHeaderRejected verifies that setting
// MaxHeaderBytes on http.Server rejects oversized non-authentication headers that
// wrapTokenHeaderSizeHandler does not inspect.
func TestTokenHeader_StdlibBackstop_NonAuthHeaderRejected(t *testing.T) {
	t.Parallel()

	core, _, _ := vault.TestCoreUnsealed(t)
	ln, addr := TestServer(t, core)
	defer ln.Close()

	oversizedValue := strings.Repeat("x", DefaultMaxTokenHeaderSize*2)

	req, err := http.NewRequest(http.MethodGet, addr+"/v1/sys/health", nil)
	require.NoError(t, err)
	req.Header.Set("X-Evil-Header", oversizedValue)

	client := cleanhttp.DefaultClient()
	client.Timeout = 15 * time.Second
	resp, err := client.Do(req)
	if err == nil {
		defer resp.Body.Close()
		require.NotEqual(t, http.StatusOK, resp.StatusCode,
			"oversized non-auth header must not reach the application layer")
	}
}

// TestTokenHeaderMaxBytes_ServerUsesCorrectDefault verifies that an http.Server
// built with TokenHeaderMaxBytes(nil) enforces the limit on any header.
func TestTokenHeaderMaxBytes_ServerUsesCorrectDefault(t *testing.T) {
	t.Parallel()

	reached := false
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reached = true
		w.WriteHeader(http.StatusOK)
	})

	srv := &http.Server{
		Handler:        inner,
		MaxHeaderBytes: TokenHeaderMaxBytes(nil),
	}

	// Start on a random port.
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	go func() {
		if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
			t.Errorf("srv.Serve: %v", err)
		}
	}()
	t.Cleanup(func() { srv.Close() })

	addr := "http://" + ln.Addr().String()

	t.Run("within_limit_reaches_handler", func(t *testing.T) {
		reached = false
		req, err := http.NewRequest(http.MethodGet, addr+"/", nil)
		require.NoError(t, err)
		req.Header.Set("X-Test", strings.Repeat("a", DefaultMaxTokenHeaderSize-200))
		resp, err := cleanhttp.DefaultClient().Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.True(t, reached, "handler should have been called for a within-limit request")
	})

	t.Run("exceeds_limit_rejected_by_stdlib", func(t *testing.T) {
		req, err := http.NewRequest(http.MethodGet, addr+"/", nil)
		require.NoError(t, err)
		// 2× to exceed the stdlib's effective limit (MaxHeaderBytes + 4096).
		req.Header.Set("X-Evil", strings.Repeat("x", DefaultMaxTokenHeaderSize*2))
		resp, err := cleanhttp.DefaultClient().Do(req)
		if err == nil {
			defer resp.Body.Close()
			require.NotEqual(t, http.StatusOK, resp.StatusCode,
				"stdlib must reject a request whose headers exceed MaxHeaderBytes")
		}
	})
}

// TestTokenHeader_ClusterListener_SkipsCheck verifies that the cluster listener
// does not re-enforce the token header size limit on forwarded requests.
// This prevents a regression where a user raising CustomMaxTokenHeaderSize above
// the default would see forwarded requests rejected by the active node's cluster
// listener, which only knows the default limit (same failure mode as the JSON
// limits regression fixed by DisableJSONLimitParsing).
func TestTokenHeader_ClusterListener_SkipsCheck(t *testing.T) {
	// A token that exceeds the default limit but would be allowed by a custom
	// API-listener limit of 16 KB. The cluster listener must pass it through
	// without re-checking.
	oversizedForDefault := strings.Repeat("a", DefaultMaxTokenHeaderSize+100)

	// Cluster listener is configured identically to how server.go sets it up:
	// DisableTokenHeaderSizeParsing = true, no CustomMaxTokenHeaderSize override.
	clusterProps := &vault.HandlerProperties{
		ListenerConfig: &configutil.Listener{
			DisableTokenHeaderSizeParsing: true,
		},
	}

	reached := false
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reached = true
		w.WriteHeader(http.StatusOK)
	})

	clusterHandler := wrapTokenHeaderSizeHandler(inner, clusterProps)

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	srv := &http.Server{Handler: clusterHandler}
	go func() {
		if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
			t.Errorf("srv.Serve: %v", err)
		}
	}()
	t.Cleanup(func() { srv.Close() })

	addr := "http://" + ln.Addr().String()

	t.Run("oversized_token_passes_cluster_listener", func(t *testing.T) {
		reached = false
		req, err := http.NewRequest(http.MethodGet, addr+"/", nil)
		require.NoError(t, err)
		req.Header.Set(consts.AuthHeaderName, oversizedForDefault)
		resp, err := cleanhttp.DefaultClient().Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()
		require.Equal(t, http.StatusOK, resp.StatusCode,
			"cluster listener must not re-enforce the token size limit on forwarded requests")
		require.True(t, reached, "inner handler should be reached on the cluster listener")
	})

	t.Run("api_listener_still_enforces_default_limit", func(t *testing.T) {
		// Confirm the same token is rejected by a normal API-listener handler
		// (no DisableTokenHeaderSizeParsing), so we know the test token really does
		// exceed the default limit.
		apiProps := &vault.HandlerProperties{
			ListenerConfig: &configutil.Listener{},
		}
		apiHandler := wrapTokenHeaderSizeHandler(inner, apiProps)

		apiLn, err := net.Listen("tcp", "127.0.0.1:0")
		require.NoError(t, err)
		apiSrv := &http.Server{Handler: apiHandler}
		go func() {
			if err := apiSrv.Serve(apiLn); err != nil && err != http.ErrServerClosed {
				t.Errorf("apiSrv.Serve: %v", err)
			}
		}()
		t.Cleanup(func() { apiSrv.Close() })

		req, err := http.NewRequest(http.MethodGet, "http://"+apiLn.Addr().String()+"/", nil)
		require.NoError(t, err)
		req.Header.Set(consts.AuthHeaderName, oversizedForDefault)
		resp, err := cleanhttp.DefaultClient().Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()
		require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode,
			"API listener must reject a token that exceeds the default size limit")
	})
}