v2.4.2
pREST v2.4.2 β HMAC jwt.key minimums, bound values for custom query scripts, credential headers withheld from templates, and SQL redacted from logs.
Released: August 11, 2026
GitHub tag Β· Compare v2.4.1...v2.4.2
v2.4.2 is a hardening release. It enforces RFC 7518 minimum key sizes for HMAC jwt.key (#1017), withholds credential headers from custom query templates and makes rejected interpolated values fail the request instead of silently emptying (#1023), stops writing caller-influenced SQL to logs, and picks up dependency updates (#1015, #1005).
Docker:
prest/prest:v2.4.2Go install:
go install github.com/prest/prest/v2/cmd/prestd@v2.4.2
Check your jwt.key length before upgrading. An HMAC key shorter than the minimum for its algorithm (32 bytes for HS256) is discarded at startup. pREST does not refuse to start β it disables /auth and the JWT middleware and keeps serving, so a short key turns an authenticated deployment into an unauthenticated one. See Upgrading from v2.4.1.
This release includes the MCP [expose] enforcement from v2.4.1, and relaxes the over-broad script-value screen that release introduced.
Highlights
Minimum HMAC key sizes for jwt.key (#1017)
pREST moved from the unmaintained square/go-jose.v2 to go-jose/go-jose/v4, which enforces RFC 7518 key sizes and returns an error for undersized HMAC keys. Rather than surface that at request time, pREST validates the key at config load:
jwt.algo
Minimum jwt.key length
HS256 (also the default when jwt.algo is unset)
32 bytes
HS384
48 bytes
HS512
64 bytes
RS*, ES*, PS*, EdDSA
Not checked β jwt.key is not used as a MAC key
The length is the byte length of the raw string, so a 32-character ASCII secret satisfies HS256.
A key below the minimum is discarded, and the features that depend on it disable themselves. Startup logs both events at ERROR:
A configured jwt.jwks or jwt.wellknownurl is unaffected β JWT verification continues against the JWKS even if the HMAC key is discarded.
See Auth β HMAC key requirements.
jwt.algo is now enforced (#1017)
jwt.algo was accepted but discarded in earlier v2 releases: tokens were parsed without restricting the permitted signature algorithm. It is now passed to the parser as the single allowed algorithm, which structurally prevents algorithm-confusion attacks.
Two consequences for existing deployments:
A token whose
algheader does not matchjwt.algois now rejected with401and{"error": "failed JWT token parser"}.The value is matched case-sensitively against a fixed set β
EdDSA,HS256,HS384,HS512,RS256,RS384,RS512,ES256,ES384,ES512,PS256,PS384,PS512. Anything else, includinghs256in lowercase or an explicitly emptyalgo = "", makes every request return HTTP 500 withunsupported JWT signature algorithm. Leavejwt.algounset to get theHS256default.
Bind values in custom query scripts (#1023)
Values a /_QUERIES template interpolates become part of the SQL text, so pREST screens them. This release makes the screen predictable and gives templates a way out of it entirely.
Bind free-form values with sqlVal, sqlList, or ident and the screen does not apply β the value travels to PostgreSQL out of band, where it can never be parsed as SQL:
{{sqlVal "key"}}
A single value
$1
{{sqlList "key"}}
A repeated query parameter (?tag=a&tag=b)
($1,$2)
{{ident "key"}}
A table or column name, which cannot be bound
"public"."users"
Three related changes:
A rejected value that is interpolated now fails the request with
400, instead of substituting an empty string and returning200with the wrong rows:The offending value is never echoed back.
sqlValandsqlListare exempt, since a bound value is never rendered into SQL text.Query parameters named
header,_param, or_headerare ignored β they are reserved for template data.
See Custom Queries β Binding values.
Credential headers withheld from templates (#1023)
A bearer token is plain base64url text, so it passed the value screen untouched and a template referencing it would interpolate the caller's credential straight into SQL β which was then logged.
These headers are now blanked before templates see them, in both the interpolated and the bound form:
Authorization Β· Proxy-Authorization Β· Cookie Β· X-Api-Key Β· X-Auth-Token Β· X-Access-Token
The request still succeeds; the value is simply empty. A template that scoped rows by the caller's token will now match nothing β move that logic to permissions or pass a non-credential header.
Other headers rejected by the screen are blanked and logged at WARN with the header name only, rather than failing the request β an ordinary User-Agent fails the character allow-list on ( and ;, so erroring would reject nearly every browser request.
SQL no longer written to logs (#1023)
Statements produced from custom query templates are caller-influenced, so they are no longer logged on either the read or the write path. CRUD statements are still logged at debug, but their parameter values are replaced by a count:
Custom query script SQL is not visible in logs at any level. Use the database's own statement logging when you need it.
Script path traversal rejected (#1023)
Script resolution now verifies that the resolved .sql file is inside the queries directory, both lexically and after resolving symlinks. .. segments and symlinks pointing outside the tree return 400 invalid script path: <folder>/<script>. This backs up the identifier validation already applied to the HTTP path, covering callers that reach the adapter directly.
Dependency updates
lestrrat-go/jwx/v3 3.1.1 β 3.2.0 (#1015) and google.golang.org/grpc 1.81.1 β 1.82.1 (#1005). Both are dependency-only with no source changes; JWKS handling and OpenTelemetry export behave as before.
Changes since v2.4.1
RFC 7518 minimum HMAC key sizes for jwt.key; go-jose/v4 migration; jwt.algo enforced as the sole permitted signature algorithm
Credential headers withheld from script templates; rejected interpolated values fail with 400; script SQL removed from logs; script path traversal rejected; #1030 screen relaxation; CI workflow permissions
Bump github.com/lestrrat-go/jwx/v3 3.1.1 β 3.2.0
Bump google.golang.org/grpc 1.81.1 β 1.82.1
Full detail: compare v2.4.1...v2.4.2. Coming from v2.4.0, see also Changes since v2.4.0.
Upgrading from v2.4.1
Measure
jwt.keyfirst.printf '%s' "$PREST_JWT_KEY" | wc -cmust be at least 32 for HS256, 48 for HS384, or 64 for HS512. Rotate the secret before deploying β a short key does not stop startup, it disables/auth(which then returns404) and passes requests through unauthenticated. After deploying, grep the startup logs forjwt.key too short.Leave
jwt.algounset unless you mean it. A value outside the supported set β including wrong case β makes every request return500. If you set it, existing tokens must be signed with that exact algorithm or they now fail with401.Audit
/_QUERIEStemplates for credential headers.{{index .header "Authorization"}}and{{sqlVal "header.Authorization"}}now render empty.Rewrite interpolated free-form values as bound values β
WHERE slug = '{{.slug}}'becomesWHERE slug = {{sqlVal "slug"}}. Search phrases are the common case: a phrase containingdo,as, ororis exactly what the screen refuses, and it now returns400rather than the wrong rows.Expect script SQL to disappear from logs. If a runbook relied on reading generated statements from
prestdoutput, switch to PostgreSQL statement logging.Deploy
prest/prest:v2.4.2, the matching binary, orgo install β¦@v2.4.2.
Coming from v2.4.0, apply the v2.4.1 upgrade notes as well β MCP discovery changes when [expose] is active.
Related
Last updated