hscontrol: limit /verify request body size

Wrap req.Body with io.LimitReader bounded to 4 KiB before
io.ReadAll. The DERP verify payload is a few hundred bytes.
This commit is contained in:
Kristoffer Dalby
2026-04-09 17:56:55 +00:00
parent a3c4ad2ca3
commit 42b8c779a0
2 changed files with 66 additions and 1 deletions

View File

@@ -80,13 +80,19 @@ func parseCapabilityVersion(req *http.Request) (tailcfg.CapabilityVersion, error
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
}
// verifyBodyLimit caps the request body for /verify. The DERP verify
// protocol payload (tailcfg.DERPAdmitClientRequest) is a few hundred
// bytes; 4 KiB is generous and prevents an unauthenticated client from
// OOMing the public router with arbitrarily large POSTs.
const verifyBodyLimit int64 = 4 * 1024
func (h *Headscale) handleVerifyRequest(
req *http.Request,
writer io.Writer,
) error {
body, err := io.ReadAll(req.Body)
if err != nil {
return fmt.Errorf("reading request body: %w", err)
return NewHTTPError(http.StatusRequestEntityTooLarge, "request body too large", fmt.Errorf("reading request body: %w", err))
}
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
@@ -124,6 +130,8 @@ func (h *Headscale) VerifyHandler(
return
}
req.Body = http.MaxBytesReader(writer, req.Body, verifyBodyLimit)
err := h.handleVerifyRequest(req, writer)
if err != nil {
httpError(writer, err)

View File

@@ -0,0 +1,57 @@
package hscontrol
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestHandleVerifyRequest_OversizedBodyRejected verifies that the
// /verify handler refuses POST bodies larger than verifyBodyLimit.
// The MaxBytesReader is applied in VerifyHandler, so we simulate
// the same wrapping here.
func TestHandleVerifyRequest_OversizedBodyRejected(t *testing.T) {
t.Parallel()
body := strings.Repeat("x", int(verifyBodyLimit)+128)
rec := httptest.NewRecorder()
req := httptest.NewRequestWithContext(
context.Background(),
http.MethodPost,
"/verify",
bytes.NewReader([]byte(body)),
)
req.Body = http.MaxBytesReader(rec, req.Body, verifyBodyLimit)
h := &Headscale{}
err := h.handleVerifyRequest(req, &bytes.Buffer{})
if err == nil {
t.Fatal("oversized verify body must be rejected")
}
httpErr, ok := errorAsHTTPError(err)
if !ok {
t.Fatalf("error must be an HTTPError, got: %T (%v)", err, err)
}
assert.Equal(t, http.StatusRequestEntityTooLarge, httpErr.Code,
"oversized body must surface 413")
}
// errorAsHTTPError is a small local helper that unwraps an HTTPError
// from an error chain.
func errorAsHTTPError(err error) (HTTPError, bool) {
var h HTTPError
if errors.As(err, &h) {
return h, true
}
return HTTPError{}, false
}