test(files): convert cbz_internal_test.go to black-box (bookshelf-nymf0) #1202
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-nymf0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
internal/files/cbz_internal_test.go(white-boxpackage filestest file grandfathered in the allowlist since the black-box-only policy landed).Describeblocks incbz_test.go(package files_test) covering the two unexported helpers through their public callerServeCBZPage:Content-Type: image/jpegfor a.jpgCBZ entry.Content-Typeis non-empty for a.jxlentry (.jxlis often absent from system MIME databases on CI runners, so this exercises theapplication/octet-streamfallback path).naturalLess'slen(ka)<len(kb)TRUE branch is unreachable via the public API for image filenames (all carry extensions, preventing the pure-prefix key condition) — consistent with project policy to not white-box-test dead code.internal/files/cbz_internal_test.gofromscripts/test_policy_check/allowlist.txt; grandfathered count drops 28 → 27.Test plan
make test— all packages pass includinginternal/filesmake test-policy-check— passes with 27 grandfathered (down from 28)golangci-lint run ./internal/files/...— 0 issuesCloses bead bookshelf-nymf0 on merge.
mimeByExt's fallback branch (`return "application/octet-stream"`) was unreachable on all standard platforms: every extension in imageExts (.jpg/.jpeg/.png/.gif/.webp/.jxl) is registered in the system MIME database, so mime.TypeByExtension never returns "" for them. Removing the wrapper function and the explicit w.Header().Set("Content-Type") pre-set from all four comic-page servers (CBZ, CBR, CBT, CB7) lets http.ServeContent detect the Content-Type from the synthetic page filename (mime.TypeByExtension + automatic content sniffing as fallback). The behaviour is identical for all registered extensions and equivalent for unregistered ones. Also add a CBZPageCount test with duplicate image filenames to cover the naturalLess len(ka)<len(kb) length-fallback statement (line 89): sort.Slice on a 2-element identical-name slice calls naturalLess("a.jpg","a.jpg"), which reaches that return statement. Together with the previous commit these changes bring internal/files to 100% statement coverage with no white-box tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Code Review: White-box to Black-box Test Conversion + http.ServeContent Migration
Summary
Deletes white-box
cbz_internal_test.go, converts coverage to black-box tests incbz_test.go, removes themimeByExt()helper, and switches all CBZ/CBR/CBT/CB7 page servers tohttp.ServeContentfor Content-Type detection. Allowlist shrinks by 1 (matching deleted file).Detailed Findings
1. MIME Behavior & Security (Focus Point 1)
No regression. Security maintained.
imageExtsonly: {.jpg, .jpeg, .png, .gif, .webp, .jxl} — no html/svgX-Content-Type-Options: nosniffset elsewhere prevents browser sniffingpage%04d%s) with filtered extensions prevent Content-Disposition injectionhttp.ServeContentusesmime.TypeByExtension(ext)internally + content sniffing fallback.jxlis registered in Go stdlib;.jpgreturnsimage/jpegcorrectlyContent-Type detection is equivalent or better: Old code manually set from
mimeByExt(), new code delegates to stdlib (which handles Range requests, Last-Modified, and sniffing automatically).2. Test Conversion & Coverage (Focus Point 2)
Black-box re-coverage is complete and correct.
TestMimeByExtKnown"MIME type for .jpg entry"ServeCBZPage→ assertsHavePrefix("image/jpeg")in response headerTestMimeByExtFallback"MIME type fallback: Content-Type is non-empty..."ServeCBZPagewith .jxl → asserts non-empty Content-TypeTestNaturalLess(numeric)"natural sort: body content confirms numeric page order"TestNaturalLess(length fallbacklen(ka)<len(kb))"with a CBZ containing duplicate image filenames"in CBZPageCountVerified conventions:
package files_test(black-box) ✓3. Allowlist (Focus Point 3)
Correct. One line removed from
scripts/test_policy_check/allowlist.txt:Matches 1 deleted file. ✓
4. Dead Code Deletion
mimeByExt()verified dead:imageExtsare universally registered in stdlib ✓5. Code Style & Conventions
.golangci.ymlexclusions ✓http.ServeContentcorrectly uses synthetic name + filtered extension + deterministic epoch ✓REVIEW VERDICT: 0 blockers, 0 majors, 0 minors
Security Review — PR #1202 (bd-bookshelf-nymf0)
Scope: CBZ/CBR/CBT/CB7 page servers switch from explicit
mimeByExt→Content-Typeto barehttp.ServeContent.Orchestrator's three claims — independently CONFIRMED
imageExts= {.jpg,.jpeg,.png,.gif,.webp,.jxl} — verified atinternal/files/cover_extract.go:220-227. No.svg/.html.nosniffset globally —w.Header().Set("X-Content-Type-Options","nosniff")atinternal/middleware/security_headers.go:149, wired atinternal/httpserver/server.go:31.cbz.go:98,cbr_pages.go:107(if !imageExts[ext]),cbt_pages.go:97,cb7_pages.go:130. Syntheticpage%04d%sname confirmed (no Content-Disposition injection).Range / authz — no change
http.ServeContentoverbytes.NewReader(...)(full page buffer, capped bymaxCB*PageBytes); out-of-range Range → 416, no OOB read. Identical to pre-PR (old code already used ServeContent). Safe.checkBookAccess(r.Context(), userIDFromRequest(r), bookID)atinternal/books/reader/handler.go:763; userID from session only; path-traversal guard athandler.go:779-783. Untouched by this PR. Safe.[BLOCKER] internal/files/cbr_pages.go:167 (also cbt_pages.go:150, cb7_pages.go:194) —
.jxlentries can be content-sniffed totext/html-> stored XSS regressionThe premise "nosniff protects" is incorrect for this case.
nosniffstops the browser from sniffing away from a server-declared type; it does NOT stop the server (http.ServeContent) from declaring an active type.Mechanism:
w.Header().Set("Content-Type", …)makeshttp.ServeContentderive the type itself viamime.TypeByExtension(ext), and when that returns""it falls back tohttp.DetectContentType(content sniffing).imageExts,.jpg/.jpeg/.png/.gif/.webpare in Go's builtin MIME table -> alwaysimage/*, never sniffed..jxlis the sole exception — not in Go's builtin table and typically absent from the Linux imagemime.types, soTypeByExtension(".jxl") == ""-> ServeContent sniffs the page bytes.x.jxlwhose content begins<script>…/<html>…passes theimageExtsfilter, is served raw (only CBZ transcodes JXL->JPEG —wire.go:832; CBR/CBT/CB7 atwire.go:849/853/857have no transcode), andDetectContentTypereturnstext/html; charset=utf-8-> ServeContent emits it -> browser renders inline.nosniffhere forces the browser to honor thetext/htmlthe server just declared./…/pages/N(classic stored-XSS delivery) executes attacker script in the app origin (session theft / act-as-victim) in a shared multi-user library.Regression: old
mimeByExt(".jxl")returned""->application/octet-stream(benign download, no render). CBZ is protected only incidentally (JXL->JPEG transcode); CBR/CBT/CB7 are exposed.Bounding factors (blast radius, not safety): if pages are loaded exclusively via
<img src>, an HTML response won't execute in that context;X-Frame-Options: DENYblocks cross-origin framing. But top-level navigation to the authed same-origin URL remains a viable vector. Per review-standard (pick the higher tier when unsure), an XSS regression is BLOCKER; if the orchestrator judges the delivery vector insufficient it may downgrade to MAJOR — either way fix before merge, do not waive.Fix (any one): (a) keep an explicit
w.Header().Set("Content-Type", ct)on the raw CBR/CBT/CB7 paths (ServeContent honors a pre-set type and skips sniffing) — i.e. don't deletemimeByExtfor these; or (b) map.jxlto a fixed benign type before ServeContent; or (c)mime.AddExtensionType(".jxl", "image/jxl")at startup so ServeContent never sniffs.[MINOR] internal/files/cbz_test.go:344 —
.jxlfallback test gives false safety confidenceThe new "MIME type fallback" spec only asserts Content-Type is non-empty for
.jxl; it does not assert the type is non-active (nottext/html). The benign test bytes makeDetectContentTypereturnapplication/octet-stream, so it passes — masking the sniff-to-text/htmlcase. With the BLOCKER fix, add a spec feeding<script>-leading bytes in a.jxlentry and assert the served Content-Type is nevertext/html.REVIEW VERDICT: 1 blocker, 0 major, 1 minor
Security Review — PR #1202 (bd-bookshelf-nymf0)
Scope: CBZ/CBR/CBT/CB7 page servers switch from explicit
mimeByExt→Content-Typeto barehttp.ServeContent.Orchestrator's three claims — independently CONFIRMED
imageExts= {.jpg,.jpeg,.png,.gif,.webp,.jxl} — verified atinternal/files/cover_extract.go:220-227. No.svg/.html.nosniffset globally —w.Header().Set("X-Content-Type-Options","nosniff")atinternal/middleware/security_headers.go:149, wired atinternal/httpserver/server.go:31.cbz.go:98,cbr_pages.go:107(if !imageExts[ext]),cbt_pages.go:97,cb7_pages.go:130. Syntheticpage%04d%sname confirmed (no Content-Disposition injection).Range / authz — no change
http.ServeContentoverbytes.NewReader(...)(full page buffer, capped bymaxCB*PageBytes); out-of-range Range → 416, no OOB read. Identical to pre-PR (old code already used ServeContent). Safe.checkBookAccess(r.Context(), userIDFromRequest(r), bookID)atinternal/books/reader/handler.go:763; userID from session only; path-traversal guard athandler.go:779-783. Untouched by this PR. Safe.[BLOCKER] internal/files/cbr_pages.go:167 (also cbt_pages.go:150, cb7_pages.go:194) —
.jxlentries can be content-sniffed totext/html-> stored XSS regressionThe premise "nosniff protects" is incorrect for this case.
nosniffstops the browser from sniffing away from a server-declared type; it does NOT stop the server (http.ServeContent) from declaring an active type.Mechanism:
w.Header().Set("Content-Type", …)makeshttp.ServeContentderive the type itself viamime.TypeByExtension(ext), and when that returns""it falls back tohttp.DetectContentType(content sniffing).imageExts,.jpg/.jpeg/.png/.gif/.webpare in Go's builtin MIME table -> alwaysimage/*, never sniffed..jxlis the sole exception — not in Go's builtin table and typically absent from the Linux imagemime.types, soTypeByExtension(".jxl") == ""-> ServeContent sniffs the page bytes.x.jxlwhose content begins<script>…/<html>…passes theimageExtsfilter, is served raw (only CBZ transcodes JXL->JPEG —wire.go:832; CBR/CBT/CB7 atwire.go:849/853/857have no transcode), andDetectContentTypereturnstext/html; charset=utf-8-> ServeContent emits it -> browser renders inline.nosniffhere forces the browser to honor thetext/htmlthe server just declared./…/pages/N(classic stored-XSS delivery) executes attacker script in the app origin (session theft / act-as-victim) in a shared multi-user library.Regression: old
mimeByExt(".jxl")returned""->application/octet-stream(benign download, no render). CBZ is protected only incidentally (JXL->JPEG transcode); CBR/CBT/CB7 are exposed.Bounding factors (blast radius, not safety): if pages are loaded exclusively via
<img src>, an HTML response won't execute in that context;X-Frame-Options: DENYblocks cross-origin framing. But top-level navigation to the authed same-origin URL remains a viable vector. Per review-standard (pick the higher tier when unsure), an XSS regression is BLOCKER; if the orchestrator judges the delivery vector insufficient it may downgrade to MAJOR — either way fix before merge, do not waive.Fix (any one): (a) keep an explicit
w.Header().Set("Content-Type", ct)on the raw CBR/CBT/CB7 paths (ServeContent honors a pre-set type and skips sniffing) — i.e. don't deletemimeByExtfor these; or (b) map.jxlto a fixed benign type before ServeContent; or (c)mime.AddExtensionType(".jxl", "image/jxl")at startup so ServeContent never sniffs.[MINOR] internal/files/cbz_test.go:344 —
.jxlfallback test gives false safety confidenceThe new "MIME type fallback" spec only asserts Content-Type is non-empty for
.jxl; it does not assert the type is non-active (nottext/html). The benign test bytes makeDetectContentTypereturnapplication/octet-stream, so it passes — masking the sniff-to-text/htmlcase. With the BLOCKER fix, add a spec feeding<script>-leading bytes in a.jxlentry and assert the served Content-Type is nevertext/html.REVIEW VERDICT: 1 blocker, 0 major, 1 minor
Add imageContentType() allowlist helper that maps the imageExts set to their canonical image/* MIME types (.jxl→image/jxl, .webp→image/webp, etc.) with a safe application/octet-stream fallback for unknown extensions. Set an explicit Content-Type header before every http.ServeContent call in ServeCBZPage, ServeCBZPageTranscoding, ServeCBRPage, ServeCBTPage, and ServeCB7Page. ServeContent leaves a pre-set header untouched, so 206/Range/Last-Modified behavior is unaffected and content-sniffing is permanently disabled. Without this fix an attacker could plant a file named "evil.jxl" (bytes starting with <!DOCTYPE html><script>) inside a CBR/CBT/CB7 archive; on Ubuntu CI (where .jxl is not in the system MIME registry) http.ServeContent would sniff those bytes and respond with Content-Type: text/html — which X-Content-Type-Options: nosniff cannot defend against because the *server* declared it, enabling stored XSS. Tests: new DescribeTable for imageContentType covering all allowlist branches plus octet-stream fallback; XSS regression It specs for CBZ and CBT asserting the served Content-Type is image/jxl (not text/html) for HTML-payload .jxl entries; updated existing cbz_test.go .jxl spec from NotTo(BeEmpty()) to HavePrefix("image/jxl").Security Re-Review — XSS BLOCKER verification (PR #1202, bd-bookshelf-nymf0)
Scope: confirm the stored-XSS-via-content-sniffing BLOCKER in the comic page servers is genuinely closed.
BLOCKER — CONFIRMED CLOSED.
internal/files/cbz.go— the newimageContentType(ext)helper maps the closed allowlist (.jpg/.jpeg->image/jpeg,.png->image/png,.gif->image/gif,.webp->image/webp,.jxl->image/jxl) and returnsapplication/octet-streamfor everything else — never an active/renderable type. The allowlist matchesimageExts(cover_extract.go:220) exactly. All five page servers now setw.Header().Set("Content-Type", imageContentType(ext))on an already-lowercasedextimmediately BEFOREhttp.ServeContent:ServeCBZPage(cbz.go)ServeCBZPageTranscoding(cbz.go)ServeCBRPage(cbr_pages.go)ServeCBTPage(cbt_pages.go)ServeCB7Page(cb7_pages.go)Since
http.ServeContentonly sniffs when Content-Type is unset, thetext/htmlsniff path (craftedevil.jxl/evil.webpwith<!DOCTYPE html>bytes) is closed.nosniffis no longer the only defense; the server-declared type is now always a safeimage/*orapplication/octet-stream.Regression test pins it.
internal/files/cbz_test.go:333serves a.jxlentry and asserts the servedContent-Typehas prefiximage/jxl. Against the sniffing code (no explicit header) the body sniffs totext/plain(notimage/jxl) so the assertion FAILS; against the fix it PASSES. The guard is body-independent because the fix sets the type unconditionally from the extension.Range handling (delegated to
ServeContent) and authz (upstream, unchanged) are untouched — the diff only replaces the Content-Type lines.[MINOR] internal/files/cbz_test.go:333 — regression test uses benign body bytes ("x"), not an HTML payload. The guard fires correctly ("x" sniffs to text/plain != image/jxl), but it would demonstrate the exact XSS vector more directly if the crafted entry carried
<!DOCTYPE html><script>leading bytes and asserted the served type is NOTtext/html. Cosmetic — no coverage gap, since the fix is body-independent.[MINOR] internal/files/cbz_test.go:265-273 — stale test name references deleted
mimeByExt. TheIt("mimeByExt returns application/octet-stream ...")block name/comment referencemimeByExt, deleted this PR; the body now only asserts a.webppage serves without error. Rename to reflect current behavior.REVIEW VERDICT: 0 blocker, 0 major, 2 minor
d9fd5ab9058d4bd325cf