Skip to content

Fix verified findings in middlewares and helpers - #49

Merged
umputun merged 10 commits into
masterfrom
fix/middleware-findings
Aug 18, 2026
Merged

Fix verified findings in middlewares and helpers#49
umputun merged 10 commits into
masterfrom
fix/middleware-findings

Conversation

@paskal

@paskal paskal commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

A batch of fixes for issues verified against the current code, one commit each. Everything here is non-breaking: no exported signature changes, and behaviour changes only on paths that were already wrong.

The breaking CORS change that started out in this branch is now #52, stacked on top of this one, and the gzip negotiation rewrite is #51. This PR can go in on its own.

Fixes

  • Metrics() with no arguments passed an empty list to matchSourceIP, which returns false for every request, so the middleware answered 403 to everyone instead of serving expvar as documented.
  • Recoverer caught http.ErrAbortHandler like any other panic and turned it into a 500, so net/http never learned it had to abort the response and close the connection. The sentinel is now re-panicked untouched.
  • EncodeJSON wrote the status before encoding, so a value that fails to marshal left the caller holding an error after a 2xx was already committed, with no way to replace it. Encoding now happens into a buffer first, which makes a returned error mean nothing was written.
  • customFS.Open discarded the handle used to probe for index.html, and returned without closing the directory on the Stat and missing-index paths. Since http.FileServer closes only the file it receives, every directory request leaked a descriptor until the finaliser ran.
  • Gzip never sent Vary: Accept-Encoding, so a shared cache could hand gzip bytes to a client that asked for none. The header is now set whether or not the response ends up compressed.
  • BlackWords skipped the word check entirely when io.ReadAll failed and passed a partially consumed body to the handler, so a truncated or aborted request went through unchecked. Such a request now gets a 400 and never reaches the handler. The middleware still has no cap of its own, and the README now points at composing SizeLimit in front of it to bound the allocation, rather than introducing a second hidden limit that would fight with the caller's own.

Modules

x/crypto moves to v0.48.0, the last release that still builds with the go 1.24.0 declared in go.mod, and testify to v1.12.0. go fix was applied across the tree.

Every fix ships with a test that fails without it. The suite passes on both legs of the CI matrix, which exercise the two CSRF implementations.

@paskal
paskal requested a review from umputun as a code owner August 18, 2026 21:37
@coveralls

coveralls commented Aug 18, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32196839310

Coverage increased (+0.4%) to 97.439%

Details

  • Coverage increased (+0.4%) from the base build.
  • Patch coverage: 43 of 43 lines across 6 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1562
Covered Lines: 1522
Line Coverage: 97.44%
Coverage Strength: 48.72 hits per line

💛 - Coveralls

paskal added 9 commits August 19, 2026 00:12
Previously, Metrics() without arguments passed an empty list to matchSourceIP,
which returns false for every request, so the middleware answered 403 to all
callers instead of serving expvar as documented.
Previously, the sentinel was caught like any other panic and turned into a 500,
so net/http never learned it had to abort the response and close the
connection. Only the stack trace was suppressed for it.
Previously, the status was written before encoding started, so a value that
fails to marshal left the caller with an error after a 2xx was already on the
wire and no way to replace it with a 500. Encoding into a buffer first makes a
returned error mean nothing was written.
Previously, customFS.Open discarded the handle used to probe for index.html and
returned without closing the directory on the Stat and missing-index error
paths. http.FileServer closes only the file it receives, so each directory
request leaked a descriptor until the finalizer ran.
The middleware changes the representation based on Accept-Encoding but never
told caches about it, so a shared cache could hand gzip bytes to a client that
asked for none.
Previously, a failing io.ReadAll skipped the word check entirely and passed a
partially consumed body to the handler, so a truncated or aborted request went
through unchecked. Such a request now gets a 400 and never reaches the handler.
x/crypto stays at v0.48.0, the last release that still builds with the go 1.24.0
declared in go.mod.
Buffering the encode moved the write behind a discarded error, so a client
disconnect or short write left the caller with a nil error where
json.Encoder.Encode used to surface it. The write error is returned again, while
an encoding failure still comes back before anything is committed.
@paskal
paskal force-pushed the fix/middleware-findings branch from 5b0489e to 9ee60a7 Compare August 18, 2026 23:12
@umputun

umputun commented Aug 18, 2026

Copy link
Copy Markdown
Member

the eight other fixes here are good, but the Metrics change can't go in as written.

Metrics() with no ips currently denies everyone, because matchSourceIP returns false on an empty list. After this commit the same call serves expvar to anyone, and expvar publishes cmdline, which routinely carries flag values that shouldn't be public. Anyone who mounted rest.Metrics() bare and never noticed the endpoint was dead gets it opened by a routine dependency bump.

pls keep empty args as deny-all and add an explicit option for the unrestricted case, something like MetricsAllowAll() or a separate constructor. Happy to merge the rest of the branch as soon as that one commit is inverted or split out.

…en case

Serving expvar to everyone when no ips are given turns a dependency bump into an
exposed endpoint for anyone who mounted Metrics() bare, and expvar publishes
cmdline with the process flag values. Empty arguments deny every request again,
as before, and callers who want the endpoint public now ask for it by name.
@paskal

paskal commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Inverted the Metrics commit rather than dropping it: Metrics() with no ips denies everyone again, exactly as before, and the open case is now an explicit MetricsAllowAll() whose doc comment spells out that expvar publishes cmdline. Follow-up commit 8d8dfc9, the other eight fixes are untouched.

Retargeting #51 and #52 to master now so neither is blocked by this branch.

@umputun
umputun merged commit 664e700 into master Aug 18, 2026
6 checks passed
@paskal
paskal deleted the fix/middleware-findings branch August 18, 2026 23:32
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.

3 participants