From 48b6c140c38a44cdcb2197b0a2ca5a3be45d86af Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Thu, 23 Apr 2026 18:32:54 -0700 Subject: [PATCH] test(handler): add FuzzHandle for the empty-payload invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handler's only caller-controlled input is the Lambda payload, and its documented contract is "accepts empty or null only — anything else is rejected so callers cannot influence token scope." Adds a native Go fuzz target that asserts this contract across adversarial bytes. Seeds include the six cases from TestHandleAcceptsOnlyEmptyPayloads plus boundary inputs (case variations, length edges, trailing NUL, surrounding whitespace, UTF-8 BOM). The oracle is derived directly from the contract: accept iff the trimmed payload is empty or literally "null". Runs as part of `moon :test` (seed corpus replays as regular subtests). A 30s live fuzz run locally produced 0 findings over 13.7M executions / 16 workers. No CI changes. Clears OpenSSF Scorecard code-scanning alert #6 (FuzzingID, 0/10) on the next Scorecard scan — the check passes when a `Fuzz*` function exists in a Go `_test.go` file. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/handler/handler_fuzz_test.go | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 internal/handler/handler_fuzz_test.go diff --git a/internal/handler/handler_fuzz_test.go b/internal/handler/handler_fuzz_test.go new file mode 100644 index 0000000..15a8398 --- /dev/null +++ b/internal/handler/handler_fuzz_test.go @@ -0,0 +1,61 @@ +package handler + +import ( + "bytes" + "context" + "encoding/json" + "io" + "log/slog" + "testing" + "time" + + "github.com/meigma/github-token-broker/internal/broker" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func FuzzHandle(f *testing.F) { + seeds := [][]byte{ + nil, + []byte("null"), + []byte(" "), + []byte("{}"), + []byte("[]"), + []byte(`"hi"`), + []byte("Null"), + []byte("NULL"), + []byte("nul"), + []byte("nulll"), + []byte("null\x00"), + []byte(" null "), + []byte("\tnull\n"), + []byte("\xef\xbb\xbf"), + []byte(`{"foo":1}`), + } + for _, s := range seeds { + f.Add(s) + } + + f.Fuzz(func(t *testing.T, payload []byte) { + h := New(&fakeBroker{ + response: broker.Response{ + Token: "ghs_fuzz", + ExpiresAt: time.Date(2026, 4, 22, 0, 0, 0, 0, time.UTC), + Repositories: []string{"acme/widgets"}, + Permissions: map[string]string{"contents": "read"}, + }, + }, slog.New(slog.NewJSONHandler(io.Discard, nil))) + + _, err := h.Handle(context.Background(), json.RawMessage(payload)) + + trimmed := bytes.TrimSpace(payload) + shouldAccept := len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) + + if shouldAccept { + require.NoError(t, err, "payload=%q should have been accepted", payload) + return + } + require.Error(t, err, "payload=%q should have been rejected", payload) + assert.ErrorContains(t, err, "does not accept invocation input") + }) +}