🐛 fix: correctness fixes for log injection, cookie jar, routing, and Content-Type handling - #4570
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR centralizes log sanitization, updates cookie-jar semantics, normalizes request headers, aligns route matching with router configuration, tightens middleware prefixes, validates BasicAuth digest sizes, and expands tests and documentation. ChangesLogging sanitization
Cookie behavior
Request normalization
Routing and middleware matching
BasicAuth validation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4570 +/- ##
==========================================
+ Coverage 93.31% 93.47% +0.16%
==========================================
Files 140 140
Lines 14858 14981 +123
==========================================
+ Hits 13864 14004 +140
+ Misses 619 608 -11
+ Partials 375 369 -6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
docs/client/examples.md (1)
231-250: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocs match the implemented limits — run the Markdown lint before merge.
The 1024/64/64 figures line up with
maxCookieJarHosts,maxCookiesPerHost, andmaxCookiesPerRequestinclient/cookiejar.go. Please runmake markdownto lint the modified Markdown.As per coding guidelines: "Run
make markdownto lint all Markdown files when modifying code".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/client/examples.md` around lines 231 - 250, Run the repository’s Markdown lint target, make markdown, after updating the cookie jar documentation, and resolve any reported issues before completing the change.Source: Coding guidelines
client/cookiejar.go (1)
576-586: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider deleting before releasing the evicted cookies.
releaseStoredCookies(evicted)hands the cookies back to the global pool whileevictedpointers are still used as identity keys in the followingDeleteFunc. It is correct today (the jar is locked and pointer comparison remains valid), but it relies on nothing else being able to observe those pointers. Swapping the order removes the subtlety at zero cost.♻️ Proposed reordering
evicted := byRecency[:overflow] - releaseStoredCookies(evicted) kept = slices.DeleteFunc(kept, func(sc storedCookie) bool { return slices.ContainsFunc(evicted, func(e storedCookie) bool { return e.cookie == sc.cookie }) }) + releaseStoredCookies(evicted)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/cookiejar.go` around lines 576 - 586, In the overflow handling block, update the eviction sequence so `kept = slices.DeleteFunc(...)` removes cookies identified by `evicted` before calling `releaseStoredCookies(evicted)`. Preserve the existing victim selection and insertion-order behavior, changing only the order of deletion and pool release.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/cookiejar.go`:
- Around line 324-352: Update SetByHost’s lookup and storage flow to account for
SetPathBytes transforming percent-encoded characters and semicolons. Apply the
same round-trip normalization guard used by setDefaultCookiePath before
searchCookieByKeyAndPath, or escape lookupPath before storing, so the lookup
path exactly matches the persisted cookie path.
In `@middleware/logger/utils.go`:
- Around line 143-157: Update writeSanitizedValue to explicitly justify ignoring
the fmt.Fprintf error, using the repository’s established //nolint:errcheck
convention and noting that the pooled byte buffer write cannot fail; preserve
the existing formatting and writeSanitized flow.
In `@path.go`:
- Line 236: Update the routeParser retrieval in the surrounding parser setup to
check the type assertion result instead of discarding it. Handle an unexpected
pool value safely before dereferencing or using parser, preserving the existing
routeParser pooling behavior for valid values.
---
Nitpick comments:
In `@client/cookiejar.go`:
- Around line 576-586: In the overflow handling block, update the eviction
sequence so `kept = slices.DeleteFunc(...)` removes cookies identified by
`evicted` before calling `releaseStoredCookies(evicted)`. Preserve the existing
victim selection and insertion-order behavior, changing only the order of
deletion and pool release.
In `@docs/client/examples.md`:
- Around line 231-250: Run the repository’s Markdown lint target, make markdown,
after updating the cookie jar documentation, and resolve any reported issues
before completing the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f26e9ffb-ac71-457a-b22f-f96a4ef94f4e
📒 Files selected for processing (38)
app_integration_test.goapp_test.gobind.gobind_test.gobinder/form.goclient/cookiejar.goclient/cookiejar_test.goctx_interface_gen.goctx_test.godocs/client/examples.mddocs/guide/utils.mddocs/middleware/basicauth.mddocs/middleware/logger.mdextractors/extractors.gohelpers.gointernal/logtemplate/sanitize.gointernal/logtemplate/sanitize_test.golisten_test.golog/context.gomiddleware/basicauth/basicauth_test.gomiddleware/basicauth/config.gomiddleware/expvar/expvar.gomiddleware/expvar/expvar_test.gomiddleware/logger/default_logger.gomiddleware/logger/logger_test.gomiddleware/logger/tags.gomiddleware/logger/utils.gomiddleware/pprof/pprof.gomiddleware/pprof/pprof_test.gopath.gopath_test.goredirect.goredirect_test.goreq.gores.gores_interface_gen.gorouter.gorouter_test.go
|
Thanks for the comprehensive fix in #4570 — the centralisation into A couple of observations after reviewing the logger-related changes:
Happy to help with any additional regression tests if useful. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
client/cookiejar.go (1)
269-274: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftAvoid routing
Set(uri, ...)throughSetByHostwhen cookies can omitPath.
Set(uri, cookies...)dropsuri.Path()and callsSetByHost(uri.Host(), cookies...), so cookies without a validPathget defaulted to/. Response parsing correctly usesdefaultCookiePathFor(path)from the request path per RFC 6265, but programmatic setters broaden those cookies host-wide. Thread the URI’s path into an internal helper while keepingSetByHost’s existing signature and behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/cookiejar.go` around lines 269 - 274, The CookieJar.Set method currently loses the request path by delegating directly to SetByHost, causing cookies without Path to default incorrectly. Add or reuse an internal helper that accepts both host and URI path, update Set to pass uri.Host() and uri.Path() through it, and keep SetByHost’s existing signature and behavior unchanged.
🧹 Nitpick comments (1)
middleware/logger/logger_test.go (1)
1815-1826: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover reset-suffix write failure.
No case sets
failAfter: 2, so the finalWriteString(reset)error path and its returned byte count are unpinned.Suggested test
+ t.Run("reset write fails", func(t *testing.T) { + t.Parallel() + + buf := &failingBuffer{ByteBuffer: bytebufferpool.Get(), failAfter: 2} + defer bytebufferpool.Put(buf.ByteBuffer) + + n, err := writeSanitizedColored(buf, color, value, reset) + require.ErrorIs(t, err, errWriteFailed) + require.Equal(t, len(color)+len(value), n) + require.Equal(t, "<c>va lue", buf.String()) + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@middleware/logger/logger_test.go` around lines 1815 - 1826, Add a test case alongside “all writes succeed” that configures failingBuffer with failAfter: 2, invokes writeSanitizedColored, and asserts the reset-suffix write error plus the returned byte count from the successful preceding writes. Verify the buffer content reflects only the color and sanitized value writes before the reset failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/cookiejar_test.go`:
- Around line 1017-1046: Update the cookie construction in
Test_CookieJar_SetByHost_ReplacesOnNormalizedPath to use fmt.Appendf for
formatting the “a=v%d; path=%s” bytes instead of fmt.Sprintf wrapped in []byte,
matching the existing modernized formatting style.
---
Outside diff comments:
In `@client/cookiejar.go`:
- Around line 269-274: The CookieJar.Set method currently loses the request path
by delegating directly to SetByHost, causing cookies without Path to default
incorrectly. Add or reuse an internal helper that accepts both host and URI
path, update Set to pass uri.Host() and uri.Path() through it, and keep
SetByHost’s existing signature and behavior unchanged.
---
Nitpick comments:
In `@middleware/logger/logger_test.go`:
- Around line 1815-1826: Add a test case alongside “all writes succeed” that
configures failingBuffer with failAfter: 2, invokes writeSanitizedColored, and
asserts the reset-suffix write error plus the returned byte count from the
successful preceding writes. Verify the buffer content reflects only the color
and sanitized value writes before the reset failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 675a6691-bbdd-4eb8-90e6-5f27cd1a066a
📒 Files selected for processing (7)
client/cookiejar.goclient/cookiejar_test.gohelpers_test.gointernal/logtemplate/sanitize_test.gomiddleware/logger/logger_test.gomiddleware/logger/utils.gopath_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- middleware/logger/utils.go
|
Addressed the outside-diff finding on
This is a behavior change worth a maintainer's eye. A cookie set via
Happy to drop this commit if you would rather keep Generated by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/cookiejar.go`:
- Around line 281-282: Update the SetByHost documentation to state that
replacement occurs only when the cookie name and normalized path match within
the same storage bucket; same-name cookies with different paths coexist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b0af47e6-5a71-4390-b6d3-49be166f4dd1
📒 Files selected for processing (2)
client/cookiejar.goclient/cookiejar_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- client/cookiejar_test.go
|
| ns/op across 6 runs | spread | |
|---|---|---|
| this branch | 15.42, 31.93, 32.53, 17.30, 16.55, 16.99 | 2.1x |
origin/main |
43.71, 15.49, 16.27, 45.62, 15.85, 15.87 | 2.9x |
origin/main, with none of these changes, swings wider than the 1.97x the alert fired on — its own run-to-run spread exceeds the reported regression. Both sides are 0 B/op and 0 allocs/op. A parallel benchmark measuring ~16 ns of work is dominated by scheduler noise on a shared runner.
No action taken. Happy to be corrected if the harness has a stabler methodology than a raw repeat count.
Also pushed in 3694fe9
${locals:}[]byterendering andenforceHostCookieLimitLocked's empty-key deletion — the four lines Codecov reported uncovered. Both were reachable, so they are covered now rather than accepted.Set/SetByHostdoc comments corrected per the review comment: replacement matches on key and normalized path within one storage scope, so same-key cookies at different paths coexist.
Generated by Claude Code
|
ok thx for the hint -> i will improve the benchmark flow |
The access-log tags that render request-controlled data (${path}, ${url},
${ua}, ${referer}, ${ip}, ${ips}, ${host}, ${body}, ${resBody},
${reqHeaders}, ${queryParams}, ${error}, ${scheme}, ${route}, and the
parametric ${reqHeader:}, ${respHeader:}, ${query:}, ${form:}, ${cookie:},
${locals:}) wrote their values verbatim. Values that reach the handler
percent-decoded — a query parameter, a form field, or c.Path() when
UnescapePath is on — can therefore carry CR/LF and forge additional log
lines, which corrupts audit trails and hides requests from SIEM tooling. A
raw request body can carry arbitrary bytes too.
C0 controls and DEL become spaces; HTAB is preserved because operators use
it to delimit structured fields. The default format is scrubbed as well:
defaultLoggerInstance short-circuits when cfg.Format == DefaultFormat and
never consults the tag map, so scrubbing only the tags would have left the
out-of-the-box configuration unprotected.
${method} is deliberately left alone. fasthttp's isValidMethod already
rejects control bytes, and Config.MethodOverride only accepts methods that
are already registered.
The helpers move to internal/logtemplate so the middleware and
log/context.go share one implementation instead of two, with the scan done
eight bytes at a time and clean input — the overwhelmingly common case —
forwarded with no copy.
Fixes #4341
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
Several related storage and retrieval defects in CookieJar. Replacement was keyed on a path prefix, so storing a cookie at "/" evicted a distinct cookie of the same name at "/admin". It now matches the exact normalized path. SetByHost also searches on the path the entry will actually carry: SetPathBytes runs the value through normalizePath, so for a cookie built by ParseBytes the stored path can differ from the one passed in, and looking up the raw form appended a duplicate instead of replacing. Cookies with no usable Path attribute are scoped to the RFC 6265 Section 5.1.4 default-path rather than "/". A Set-Cookie from "/foo/bar" is no longer sent to "/foo/barbaz". Set(uri, ...) applies the same rule, which is a behavior change: a cookie set against "/a/b" with no Path is now scoped to "/a" rather than host-wide. Set Path explicitly to keep the old scope. SetByHost, SetKeyValue and SetKeyValueBytes have no request path to derive a scope from and are unchanged. A Path that does not begin with '/' is unusable per Section 5.2.4 and falls back the same way, instead of being stored verbatim where it could never match a request path while still occupying a per-host slot. Cookies are sent longest-path-first per Section 5.4 with a creation-order tiebreak, so the most specific value wins deterministically rather than depending on map iteration order. Per-host storage is bounded with recency-based eviction (Section 5.3), expired entries going first, and the number of cookies one request may carry is bounded separately in dumpCookiesToReq — that cap belongs to the wire, and applying it in cookiesForRequest also capped the exported Get, which promises every stored cookie matching the URI. Finally, the jar no longer case-folds the caller's cookie Domain in place; it documents that it stores copies, so mutating the caller's struct broke that contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
Bind().Body() lowercased the Content-Type header inside the request's own storage, so the mutation outlived the call and was visible to every later reader of that request — a handler inspecting c.Get(HeaderContentType), a proxy forwarding the request, or anything comparing the value case-sensitively. Body() did the same to Content-Encoding. Fold into scratch instead, and fold only what RFC 9110 says is case-insensitive: the media type (Section 8.3.1) and each parameter name (Section 5.6.6). Parameter values are left byte-for-byte alone, which matters because a multipart/form-data boundary is case-sensitive and folding it broke multipart parsing outright. Quoted-strings are consumed as a unit so a ';' inside one does not mislocate the next parameter name. The same normalization is applied in Bind().Form() and Redirect().WithInput(), which reach the media type through their own paths, and binder/form.go compares with EqualFold so a mixed-case media type binds the same as a lowercase one. utilsstrings.ToLower returns its input unchanged when there is no uppercase byte, so the common path stays allocation-free. A stack scratch buffer is not usable for the Content-Encoding case: the substrings flow into tryDecodeBodyInOrder, which forces the array to the heap on every call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
getMatch lets a leading constant segment with HasOptionalSlash match one byte less than its Const, so "/a/" also matches the detection path "/a". Any detection path shorter than maxDetectionPaths hashes to bucket 0, so a route bucketed under the hash of its full three-byte constant was invisible to that request: next() scanned bucket 0 and never saw it. "/a/:id?" and "/a/*" therefore did not match "/a", while the otherwise identical "/ab/:id?" and "/ab/*" matched "/ab" — the bug only bites when the constant is exactly maxDetectionPaths bytes, because a longer one still shares its first three bytes after the trailing '/' is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
RoutePatternMatch says "see logic in (*Route).match and (*App).register", but it diverged from both, so the helper answered differently than the router it is meant to predict. It trimmed trailing slashes from the pattern but not from the path, so "/a/" did not match "/a" under non-strict routing. It lowercased the same string it sliced parameter values out of, so a case-sensitive constraint such as regex(^[A-Z]+$) saw folded bytes the router never sees. It skipped UnescapePath decoding entirely. And its root/star shortcut compared the raw pattern, so an escaped literal "/\*" was treated as a star route because register derives those flags from the escape-stripped form. It now mirrors DefaultCtx.configDependentPaths: decode when UnescapePath is set, derive a separate detection path for matching while keeping the untouched path for slicing values, trim both sides under non-strict routing, and compare the escape-stripped pattern in the shortcut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
Res.Cookie normalized the caller's *Cookie in place — lowercasing SameSite, and forcing Path and Secure for a partitioned cookie — so a caller reusing one struct across responses saw its own values rewritten underneath it. A template cookie mutated on first use then carried the mutation into every later response built from it. Normalize a local copy instead. The interface docs are regenerated to state that the argument is not modified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
A {SHA256} or {SHA512} password entry was accepted whenever its base64
decoded successfully, without checking that the result is the digest's
length. A truncated hash therefore loaded fine and was compared against a
full-length digest, so it could never match — a silently unusable
credential rather than a startup error.
Both prefixes now validate the decoded length, and
ErrInvalidSHA512PasswordLength is added alongside the existing SHA256 one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
Both middlewares matched their prefix with a bare string comparison, so any path merely starting with those bytes was swallowed. An application route at /debug/varsdump or /debug/pprofiler was answered by the diagnostic handler instead of the application, and with pprof's custom Prefix the blast radius is whatever the user configured. Match only when the prefix is followed by '/' or ends the path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
require.ErrorAs(t, err, &ErrRedirectBackNoFallback) passes the address of a package-level sentinel as the target, so errors.As writes the matched error back into that global. Test_Redirect_Back and the parallel Test_Redirect_Back_WithCrossOriginReferer then write and read ErrRedirectBackNoFallback concurrently, which the race detector reports as a data race in (*Redirect).Back. The assertion was also vacuous. ErrRedirectBackNoFallback is a *Error, so the target is a **Error and errors.As matches any fiber *Error in the chain: pointing it at ErrBadRequest instead still passes. ErrorIs compares against the sentinel itself, which is what the test means and what the sibling assertion in Test_Redirect_Back_WithCrossOriginReferer already does. Reproduced with `-race -run Test_Redirect -count=20`: 15 of 15 runs failed before, 0 of 15 after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
The comment claimed the scheme is matched case-insensitively. It is not — the comparison is exact, which is the intended behavior. Correct the comment rather than the code, so the documented contract matches the one callers get. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
9df05f1 to
57f001d
Compare
…er them The benchmark pipeline drops parallel results before comparing, on both layers, by matching the name against _Parallel. These ten sub-benchmarks spell it as NoProxyCheckParallel and WithProxyCheckParallelMultipleSubnet, without the underscore, so neither layer matched and they were the only b.RunParallel results in the suite reaching the regression comparison. They are also the fastest benchmarks in the suite, 4.34 to 39.31 ns/op. At that scale b.RunParallel on a shared 4 vCPU runner measures how many cores the job happened to get, so the numbers swing 2 to 3x run to run and trip the 150% alert threshold on unrelated PRs. Seen most recently on #4570, where WithProxyCheckParallelMultipleSubnet was reported 1.97x worse (16.46 to 32.43 ns/op) on a branch that does not touch IsProxyTrusted. Insert the underscore so the existing filter matches. Nothing else changes: the benchmarks still run and still report, their results just stop being compared against a baseline that cannot be measured this precisely on shared hardware. Note this only fixes the ten that carry Parallel in the name. Benchmark_Router_GitHub_API and BenchmarkURLRoundrobinGet/{atomic,mutex} also use b.RunParallel without saying so in their names, so no name based filter can reach them.
Outdated benchmark report, superseded by a newer run.
|
| Benchmark | Package | Base | Current | Worse by |
|---|---|---|---|---|
BenchmarkMarshalMsgresponse |
github.com/gofiber/fiber/v3/middleware/idempotency |
73.81 ns/op | 125.3 ns/op | 1.70x |
BenchmarkResolvePolicy_Override |
github.com/gofiber/fiber/v3/middleware/proxy |
68.94 ns/op | 110.8 ns/op | 1.61x |
Improvements (at least 1.50x)
| Benchmark | Package | Base | Current | Better by |
|---|---|---|---|---|
Benchmark_Ctx_SendFile |
github.com/gofiber/fiber/v3 |
12748 ns/op | 5976 ns/op | 2.13x |
Measured on Ampere-1a (GOMAXPROCS=4).
Compared 1584 of 1584 results against the base branch.
|
The The latest run flags two different ones, though, and both are in packages this PR does not touch:
Neither
Its base value alone moved 5301 → 12748 across runs, so the baseline is drifting as much as the measurement. Flagging in case it is useful input for the harness work — no action needed on this PR's side. Generated by Claude Code |
There was a problem hiding this comment.
Docs check out on the facts. The tag list matches tags.go, and the ${method}/${protocol} reasoning holds against fasthttp's request-line parsing. A few points on completeness and placement:
The caution has no supported API behind it. It asks readers to sanitize the values they write from a custom tag, but nothing is exported for that: internal/logtemplate is not importable and utils/v2 has no equivalent. Exporting a helper, or inlining a short strings.Map snippet, would make it actionable.
The warning is far from the code it applies to. The custom tag guide at logger.md:148 and the CustomTags example at :192 both show plain output.WriteString(...); the caution appears at :300. A pointer at both spots would connect them.
RegisterContextTag and LoggerFunc are not covered. context_tag.go:57/:65 writes unscrubbed, and the built-in tags on that path are safe only because each middleware sanitizes at the source. LoggerFunc replaces the pipeline entirely. Separately, the comment at logger.go:124 refers custom implementations to logtemplate.ExecuteChains, which is internal and not importable.
The CookieJar.Set change has no migration note in the docs. It is flagged IMPORTANT in the description, but whats_new.md is untouched and examples.md covers only the Set-Cookie side, so the note currently lives only in the PR body.
Minor: TagScheme is absent from the constants block at :327, leaving ${scheme} introduced by the new section and defined nowhere else. C1 controls are not scrubbed, while basicauth does strip them. The new section is also the only hard-wrapped paragraph in the file.
Review found the new sanitization section accurate but incomplete, and resting on advice the framework gave no way to follow. Export logger.SanitizeValue. The caution told readers to sanitize values they write from a custom tag, but nothing public did that: internal/logtemplate is not importable, sanitizeLogValue and writeSanitizedString are unexported, and gofiber/utils/v2 has no equivalent. The helper applies exactly what the built-in tags apply, so the guidance and the implementation cannot drift. Name every path that bypasses the built-in scrubbing. The caution listed Config.CustomTags and RegisterTag; RegisterContextTag also writes unscrubbed (context_tag.go), and Config.LoggerFunc replaces the rendering pipeline wholesale (logger.go), which is the wider hole of the two. Fiber's own context tags are safe because each middleware validates or redacts at the source, which a reader could not infer, so say so. Put the warning where the code is. "Register Tags from Custom Middleware" and the CustomTags example both teach output.WriteString(value) verbatim around 150 lines above the caution, so a reader following either never reached it. Both now point at the section. State the ASCII limit. C1 controls (U+0080-U+009F, including NEL U+0085, which some log pipelines treat as a line break) pass through untouched, unlike basicauth's containsCTL, which strips them via unicode.IsControl. Document the CookieJar.Set scoping change in whats_new.md, next to the Cookie Jar migration row. It was only in the PR description and the godoc, so nobody upgrading would have found it. Also: add the missing TagScheme to the Constants block, match the heading case of its siblings, unwrap the one hard-wrapped paragraph in a file that is otherwise one line per paragraph, and stop the LoggerFunc comment naming logtemplate.ExecuteChains, which callers cannot reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6
|
Thanks — that's a thorough read, and every finding held up when I checked it against the code. All addressed in ede8793. 1. Caution asked for something unavailable. Correct, and it was the worst of the five: the section told people to sanitize with no supported way to do it. I went with exporting rather than an inline snippet — 2. Placement. Both sites now point at the section: a line after the 3. 4. 5. Smaller: One thing I did not do, since it is a security-behavior call rather than a docs one: extend the scrub set to C1. NEL is a real forging vector against pipelines that treat it as a line break, so it is arguably in scope for #4341 — but it would change the SWAR fast path and wants its own benchmark. Happy to do it here or as a follow-up, whichever you prefer. Verification: Generated by Claude Code |
Description
This branch collects a set of correctness fixes found while auditing the framework. They are independent of one another and each ships with a regression test that was checked to fail when its fix is reverted.
The headline issue is log injection in the logger middleware. Access-log tags that render request-controlled data wrote their values verbatim, so any value that reaches the handler percent-decoded — a query parameter, a form field, or
c.Path()underUnescapePath— could carry CR/LF and forge additional log lines, corrupting audit trails and hiding requests from SIEM tooling.Important
One change alters observable behavior for existing callers:
CookieJar.Set(uri, ...)now scopes a cookie with noPathattribute to the URI's RFC 6265 §5.1.4 default-path instead of/. See the cookie-jar section below.Fixes #4341
Changes introduced
Logger — log injection (#4341)
${path},${url},${ua},${referer},${ip},${ips},${host},${body},${resBody},${reqHeaders},${queryParams},${error},${scheme},${route}, and the parametric${reqHeader:},${respHeader:},${query:},${form:},${cookie:},${locals:}.defaultLoggerInstanceshort-circuits whencfg.Format == DefaultFormatand never consults the tag map, so the initial fix left the out-of-the-box configuration — the one most deployments use — unprotected.${method}is deliberately not scrubbed: fasthttp'sisValidMethodalready rejects control bytes, andConfig.MethodOverrideonly accepts registered methods.internal/logtemplateso the logger middleware andlog/context.goshare one implementation instead of two.Client cookie jar — RFC 6265 conformance
Set-Cookieto the RFC 6265 §5.1.4 default-path rather than the request path, soSet-Cookie: a=1from/foo/baris no longer sent to/foo/barbaz.Set(uri, ...)applies that same default-path. It previously droppeduri.Path()and delegated toSetByHost, so a cookie with no usablePathwas scoped to/— the identical cookie arriving as aSet-Cookieheader and set programmatically ended up with different scopes. A cookie set against/a/bwith noPathis now scoped to/arather than host-wide; setPathexplicitly to keep the old scope.SetByHost,SetKeyValue, andSetKeyValueByteshave no request path to derive a scope from and are unchanged.Domainin place — the jar was mutating a struct the caller still owned.SetByHostsearches on the path the entry will actually carry.SetPathBytesruns the value throughnormalizePath, so for a cookie built byParseBytesthe stored path can differ from the one passed in; looking up the raw form missed the entry an earlier identical call created and appended a duplicate instead of replacing it, consuming per-host slots until eviction dropped a legitimate cookie.maxCookiesPerRequestapplies indumpCookiesToReqrather thancookiesForRequest. The cap is a wire concern, butcookiesForRequestalso backs the exportedGet, which silently returned at most 64 cookies while documenting that it returns those stored for the URI.Content-Type handling
Bind().Body()lowercased theContent-Typeheader in the request's own storage, so the mutation was visible to every later reader of that request.multipart/form-databoundary is case-sensitive and folding it broke multipart parsing.Bind().Form()andRedirect().WithInput(), which reached the media type through separate paths.Routing
maxDetectionPathsbytes (/a/) can match a detection path one byte shorter (/a), and that shorter path always hashes to bucket 0 — so/a/:id?and/a/*were unreachable for/a, while the otherwise identical/ab/...routes matched.RoutePatternMatcha faithful mirror of the router, as its own documentation claims. It trimmed trailing slashes from the pattern but not from the path, lowercased the path it also sliced parameter values from, skippedUnescapePathdecoding, and compared the un-escape-stripped pattern in its root/star shortcut.Other fixes
Res.Cookietreats its argument as read-only. It normalized the caller'sCookiestruct in place, so a caller reusing one struct across responses saw its values rewritten.basicauthrejects{SHA256}/{SHA512}hashes whose decoded length is not the digest size, instead of accepting a truncated hash.expvarandpprofonly claim paths at a segment boundary, so/debug/varsdumpand/debug/pprofilerfall through to the application instead of being swallowed.Test and tooling
Test_Redirect_Back.require.ErrorAs(t, err, &ErrRedirectBackNoFallback)passes the address of a package-level sentinel, soerrors.Aswrites into that global while a parallel test reads it. The assertion was also vacuous: the target is a**Error, so it matched any fiber*Error— pointing it atErrBadRequestpassed just as well.ErrorIsagainst the sentinel is what the test means.WriteSanitized75→100,ScrubControls87.5→100,writeSanitizedColored77.8→100,samePath60→100,pathMatch81.8→100,CookieJar.Get75→100,CookieJar.Set66.7→100,normalizeContentTypeMediaType92.9→100,RoutePatternMatch94.3→100, plus both halves ofenforceHostCookieLimitLockedand every arm of${locals:}'s type switch.Note
An earlier commit here relaxed
app.Test's 1s default timeout and the listener-readiness budgets to stop three tests flaking under-race. That has been reverted — the timeout flakes are being handled separately. The measurements are preserved in the revert commit message for whoever picks them up: aDefaultBodyLimit-sized round trip measured 198ms–1.29s under-raceagainst the 1s default, and the listener waits arerequire.Eventuallyaround a blockingInmemoryListener.Dial, so their duration only matters when the server never starts.utilsstrings.ToLowerreturns its input unchanged when there is no uppercase byte, and the cookie-jar dedupe scan avoids allocating.docs/middleware/logger.md,docs/middleware/basicauth.md,docs/guide/utils.md,docs/client/examples.md.CookieJar.Setscoping change belongs in the release notes.CookieJar.Setneeds one, and it is a one-liner — pass an explicitPathon the cookie to keep the previous host-wide scope. No signature changes, so nothing else has to move.basicauth.ErrInvalidSHA512PasswordLengthis the only addition to the public surface; the newinternal/logtemplatehelpers are not importable outside the module.docs/client/examples.mdcovers the cookie-jar behavior.Type of change
Checklist
/docs/directory for Fiber's documentation.-race.🤖 Generated with Claude Code
https://claude.ai/code/session_011DMnryEc8UAkz73vAeMBf6