Skip to content

fix: reject NUL bytes and invalid UTF-8 in text parameters with a 400 - #1143

Merged
alexluong merged 1 commit into
hookdeck:mainfrom
Mohil-Ahuja:fix/1114-invalid-text-params
Oct 13, 2026
Merged

alexluong merged 1 commit into
hookdeck:mainfrom
Mohil-Ahuja:fix/1114-invalid-text-params

Conversation

@Mohil-Ahuja

Copy link
Copy Markdown
Contributor

Fixes #1114

A NUL byte or invalid UTF-8 in a path or query parameter, or in the position of a hand-built cursor, now gets a 400 on every log store instead of a 500 from PostgreSQL (SQLSTATE 22021).

Change

  • TextParamsMiddleware (internal/apirouter/textparams_middleware.go) runs after auth on every API route. A path or query value holding a NUL byte or invalid UTF-8 is answered with a 400 that names the parameter, in the same style as the limit errors: invalid tenant_id: must be valid UTF-8 without NUL bytes. It covers the list filters, the tenant_id query on the retrieve endpoints, the metrics filters and path IDs like /events/:event_id, so nothing like that reaches a store. Valid non-ASCII text is untouched.
  • cursor.Decode treats a position with a NUL byte or invalid UTF-8 as invalid cursor. The cursor itself is base62, so it gets through the middleware; the check has to happen after decoding. This gives the same 400 on PostgreSQL (500 before) and ClickHouse (200 before).

I put the check in the router rather than in pglogstore so all log stores answer the same way, which is what the issue asks for. It also covers path parameters that go to the tenant store.

Tests

  • TestAPI_InvalidTextParamReturns400: NUL and invalid UTF-8 in query values, in one value of an array parameter, in path parameters (including nested ones) and in a metrics filter.
  • TestAPI_ValidTextParamIsAccepted: plain and non-ASCII values still list normally.
  • TestAPI_InvalidCursorReturns400: two new cases, a position with a NUL byte and one with invalid UTF-8, on all four lists.
  • TestDecode: the same at the cursor package level.

On the previous code the new API tests fail (200 or 404 on the mem store, where PostgreSQL gave the 500), and the tenant cursor cases give invalid cursor: invalid timestamp. With the change, go test -short ./internal/apirouter/ ./internal/cursor/ passes, along with go build ./..., go vet and gofmt. I didn't run the PostgreSQL integration suite locally; the change sits in front of the store, so the store code is unchanged.

PostgreSQL refuses a NUL byte or invalid UTF-8 in a text parameter
(SQLSTATE 22021), so GET /api/v1/events?tenant_id=%00 and similar
requests failed with a 500 on the PostgreSQL log store.

TextParamsMiddleware runs after auth on every API route and answers a
path or query parameter holding such a value with a 400 naming it. A
cursor whose decoded position holds one is now an invalid cursor, so a
hand-built cursor gets the same 400 on every log store instead of a 500
on PostgreSQL and a 200 on ClickHouse.

Fixes hookdeck#1114

@alexluong alexluong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your first contribution to Outpost, and welcome! This looks good to me, merging it in.

@alexluong
alexluong merged commit dbc4203 into hookdeck:main Oct 13, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL log store returns 500 for a NUL byte or invalid UTF-8 in a text parameter

2 participants