Found in the independent review of #131. Not milestoned 1.0.0: the page is authenticated and the size is bounded and paginated, so by the tag's blocking rule it belongs to the next cycle. It is filed because #131 changed the memory profile of this page and that change should not go unrecorded.
templates/source_logs.html:39 renders {{.Body}} untruncated. With paginationPerPage = 25 (internal/handlers/handlers.go:31) and the 1 MB ingest cap (internal/handlers/webhook.go:17), one page can carry ~25 MB of raw bodies — before HTML escaping, which inflates adversarial payloads several-fold (html/template escapes + to + and similar).
Before #131 this streamed to the socket, so the peak was small. That PR buffers the whole page in memory to fix partial-page renders (#123) — the right trade, but it converts this page's size from a bandwidth cost into a resident-memory cost of up to tens of MB per concurrent viewer.
REPO_POLICIES.md:297 asks for a maximum response size where applicable, which this page arguably is.
Definition of done
The event-log page truncates each rendered body to a fixed cap (8 KB is a reasonable starting point) with a visible "truncated" marker, so page size is bounded by the cap rather than by stored payload size.
The full body remains retrievable — truncation is a rendering concern, not a data-loss one. Say in the PR where the untruncated value is still reachable, or file the follow-up if it is not.
A test asserting the cap holds for a stored body far above it.
Implementation requirements
Branch from next, PR based on next, single commit, title ending (closes #N).
Gate on make check plus the Docker lint path with the cache defeated (#119).
Found in the independent review of https://git.eeqj.de/sneak/webhooker/pulls/131. Not milestoned 1.0.0: the page is authenticated and the size is bounded and paginated, so by the tag's blocking rule it belongs to the next cycle. It is filed because https://git.eeqj.de/sneak/webhooker/pulls/131 changed the memory profile of this page and that change should not go unrecorded.
`templates/source_logs.html:39` renders `{{.Body}}` untruncated. With `paginationPerPage = 25` (`internal/handlers/handlers.go:31`) and the 1 MB ingest cap (`internal/handlers/webhook.go:17`), one page can carry ~25 MB of raw bodies — before HTML escaping, which inflates adversarial payloads several-fold (`html/template` escapes `+` to `+` and similar).
Before https://git.eeqj.de/sneak/webhooker/pulls/131 this streamed to the socket, so the peak was small. That PR buffers the whole page in memory to fix partial-page renders (https://git.eeqj.de/sneak/webhooker/issues/123) — the right trade, but it converts this page's size from a bandwidth cost into a resident-memory cost of up to tens of MB per concurrent viewer.
`REPO_POLICIES.md:297` asks for a maximum response size where applicable, which this page arguably is.
## Definition of done
- The event-log page truncates each rendered body to a fixed cap (8 KB is a reasonable starting point) with a visible "truncated" marker, so page size is bounded by the cap rather than by stored payload size.
- The full body remains retrievable — truncation is a rendering concern, not a data-loss one. Say in the PR where the untruncated value is still reachable, or file the follow-up if it is not.
- A test asserting the cap holds for a stored body far above it.
## Implementation requirements
- Branch from `next`, PR based on `next`, single commit, title ending ` (closes #N)`.
- Do not modify `TODO.md` (see https://git.eeqj.de/sneak/webhooker/issues/112).
- Gate on `make check` plus the Docker lint path with the cache defeated (https://git.eeqj.de/sneak/webhooker/issues/119).
clawbot
self-assigned this 2026-08-12 11:56:52 +02:00
Plan: bound the body in the query layer, not the template.
loadEventsWithDeliveries will stop loading []database.Event and instead select a projection whose body column is substr(cast(body as blob), 1, 8192), plus length(cast(body as blob)) for the real size. The cast makes both byte-wise rather than character-wise, so the bound is bytes. A stored body over the cap therefore never becomes a Go string at all — the buffered render is bounded by cap x page size regardless of what was ingested, which template-side truncation would not achieve (it would still materialise the full string, and truncating post-escape would be worse still).
SQLite cuts at an arbitrary byte, so the tail can be a partial UTF-8 sequence. A helper walks back at most utf8.UTFMax bytes to the last utf8.RuneStart byte and drops that sequence if utf8.FullRune says it is incomplete; bytes that are merely invalid UTF-8 (a binary payload) are left alone.
Events become EventLogView alongside the existing DeliveryView/TargetView projections, carrying BodyTruncated and BodyBytes so templates/source_logs.html can show a visible marker with the real byte count.
Also corrects the now-false "these pages are small" comment at internal/handlers/handlers.go:232.
Plan: bound the body in the query layer, not the template.
`loadEventsWithDeliveries` will stop loading `[]database.Event` and instead select a projection whose body column is `substr(cast(body as blob), 1, 8192)`, plus `length(cast(body as blob))` for the real size. The cast makes both byte-wise rather than character-wise, so the bound is bytes. A stored body over the cap therefore never becomes a Go string at all — the buffered render is bounded by cap x page size regardless of what was ingested, which template-side truncation would not achieve (it would still materialise the full string, and truncating post-escape would be worse still).
SQLite cuts at an arbitrary byte, so the tail can be a partial UTF-8 sequence. A helper walks back at most `utf8.UTFMax` bytes to the last `utf8.RuneStart` byte and drops that sequence if `utf8.FullRune` says it is incomplete; bytes that are merely invalid UTF-8 (a binary payload) are left alone.
Events become `EventLogView` alongside the existing `DeliveryView`/`TargetView` projections, carrying `BodyTruncated` and `BodyBytes` so `templates/source_logs.html` can show a visible marker with the real byte count.
Also corrects the now-false "these pages are small" comment at `internal/handlers/handlers.go:232`.
clawbot
added this to the 1.0.0 milestone 2026-08-17 22:32:39 +02:00
Moved INTO the 1.0.0 milestone, reversing the "Not milestoned" line in the body above.
Reason the original call no longer holds: "the page is authenticated" bounds who TRIGGERS the render, not who SUPPLIES the payload. The bodies come from the unauthenticated public receiver, so an attacker fills a page with 25 x 1 MB bodies for free and the operator detonates it by opening the event log — several times that resident after escaping inflation, per concurrent viewer. Remote attacker-controlled memory exhaustion with an ordinary admin action as the trigger is on the wrong side of the 1.0 bar, and REPO_POLICIES.md:297 already asks for a bounded response size here.
The other half is timing: when this was filed, #123 had not landed and renderTemplate still streamed to the socket, so the exposure was hypothetical. 123 is now on next (0b457ea), so it is live.
The plan in the comment above is approved as written — bound it in the query layer via substr(cast(body as blob), 1, 8192) rather than in the template, so an oversized body never becomes a Go string. Template-side truncation would still materialise the full value and miss the point.
Moved INTO the `1.0.0` milestone, reversing the "Not milestoned" line in the body above.
Reason the original call no longer holds: "the page is authenticated" bounds who TRIGGERS the render, not who SUPPLIES the payload. The bodies come from the unauthenticated public receiver, so an attacker fills a page with 25 x 1 MB bodies for free and the operator detonates it by opening the event log — several times that resident after escaping inflation, per concurrent viewer. Remote attacker-controlled memory exhaustion with an ordinary admin action as the trigger is on the wrong side of the 1.0 bar, and `REPO_POLICIES.md:297` already asks for a bounded response size here.
The other half is timing: when this was filed, https://git.eeqj.de/sneak/webhooker/issues/123 had not landed and `renderTemplate` still streamed to the socket, so the exposure was hypothetical. `123` is now on `next` (`0b457ea`), so it is live.
The plan in the comment above is approved as written — bound it in the query layer via `substr(cast(body as blob), 1, 8192)` rather than in the template, so an oversized body never becomes a Go string. Template-side truncation would still materialise the full value and miss the point.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found in the independent review of #131. Not milestoned 1.0.0: the page is authenticated and the size is bounded and paginated, so by the tag's blocking rule it belongs to the next cycle. It is filed because #131 changed the memory profile of this page and that change should not go unrecorded.
templates/source_logs.html:39renders{{.Body}}untruncated. WithpaginationPerPage = 25(internal/handlers/handlers.go:31) and the 1 MB ingest cap (internal/handlers/webhook.go:17), one page can carry ~25 MB of raw bodies — before HTML escaping, which inflates adversarial payloads several-fold (html/templateescapes+to+and similar).Before #131 this streamed to the socket, so the peak was small. That PR buffers the whole page in memory to fix partial-page renders (#123) — the right trade, but it converts this page's size from a bandwidth cost into a resident-memory cost of up to tens of MB per concurrent viewer.
REPO_POLICIES.md:297asks for a maximum response size where applicable, which this page arguably is.Definition of done
Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).TODO.md(see #112).make checkplus the Docker lint path with the cache defeated (#119).Plan: bound the body in the query layer, not the template.
loadEventsWithDeliverieswill stop loading[]database.Eventand instead select a projection whose body column issubstr(cast(body as blob), 1, 8192), pluslength(cast(body as blob))for the real size. The cast makes both byte-wise rather than character-wise, so the bound is bytes. A stored body over the cap therefore never becomes a Go string at all — the buffered render is bounded by cap x page size regardless of what was ingested, which template-side truncation would not achieve (it would still materialise the full string, and truncating post-escape would be worse still).SQLite cuts at an arbitrary byte, so the tail can be a partial UTF-8 sequence. A helper walks back at most
utf8.UTFMaxbytes to the lastutf8.RuneStartbyte and drops that sequence ifutf8.FullRunesays it is incomplete; bytes that are merely invalid UTF-8 (a binary payload) are left alone.Events become
EventLogViewalongside the existingDeliveryView/TargetViewprojections, carryingBodyTruncatedandBodyBytessotemplates/source_logs.htmlcan show a visible marker with the real byte count.Also corrects the now-false "these pages are small" comment at
internal/handlers/handlers.go:232.Moved INTO the
1.0.0milestone, reversing the "Not milestoned" line in the body above.Reason the original call no longer holds: "the page is authenticated" bounds who TRIGGERS the render, not who SUPPLIES the payload. The bodies come from the unauthenticated public receiver, so an attacker fills a page with 25 x 1 MB bodies for free and the operator detonates it by opening the event log — several times that resident after escaping inflation, per concurrent viewer. Remote attacker-controlled memory exhaustion with an ordinary admin action as the trigger is on the wrong side of the 1.0 bar, and
REPO_POLICIES.md:297already asks for a bounded response size here.The other half is timing: when this was filed, #123 had not landed and
renderTemplatestill streamed to the socket, so the exposure was hypothetical.123is now onnext(0b457ea), so it is live.The plan in the comment above is approved as written — bound it in the query layer via
substr(cast(body as blob), 1, 8192)rather than in the template, so an oversized body never becomes a Go string. Template-side truncation would still materialise the full value and miss the point.clawbot referenced this issue2026-08-17 23:18:15 +02:00
clawbot referenced this issue2026-08-17 23:50:11 +02:00