The plugin lane stops being a way in — 37 of the 38 security-review findings #58

Merged
enricobuehler merged 3 commits from worktree-security-review-0805-fixes into main 2026-08-05 17:39:03 +00:00
Owner

Acts on the 2026-08-05 host security review (punktfunk-planning/design/security-review-2026-08-05.md). 37 of its 38 findings; the one exception is called out at the bottom.

Three commits, because the first two are two different arguments:

  • 81039581 — the plugin authorization lane, the Windows local-EoP pair, the CI substrate, and the whole Low tier.
  • defdfbdb — H-3, which needed a change of shape rather than a change of value.
  • 0d94ef0d — a clippy fix CI caught in the first commit (Result<(), Response>Option<Response>; the "error" is the response, so there was never an error value to propagate).

The plugin lane

plugin_may_access was the only authorization gate in the system that was allow-by-default: a hand-maintained denylist of route prefixes, where every sibling gate (cert_may_access, the QUIC pairing gate, the console's isPublicPath) is deny-by-default. Its own doc comment names the two capabilities it exists to withhold, and both were reachable one route over — not because anyone drifted, but because ~1450 commits of new routes arrived and a denylist is not a thing people remember to update.

It is an allowlist now, and every_route_is_classified_for_the_plugin_and_cert_lanes walks the live route table out of openapi_json() and fails the build for any route not deliberately classified for both non-admin lanes. That test is the actual fix — the gate closes today's hole, the test closes the class.

Route reachability and field authority turned out to be separate questions. A provider plugin must be able to call PUT /library/provider/{p} — reconciling its own entry set is what a scanner plugin is — but prep and launch.kind == "command" inside that payload are handed to /bin/sh -c (or cmd.exe /c) as the host user, and every execution site documents them as operator-typed. So requests now carry the lane that authorized them (AuthLane) and the handlers refuse those two fields to everyone but the operator's own token. A route gate cannot express that.

The art proxy read any absolute path off disk in the host process — LocalSystem on Windows — from a path the plugin lane could write and then read back, which yielded mgmt-token and with it full admin. It now serves only real images (known extension and magic bytes, so a renamed secret fails), only from inside an allowed root, only after canonicalization, never over UNC; and a path it would refuse to serve can no longer be persisted in the first place.

Windows

The config-dir hardening was skipped exactly when it was needed: it ran only in the branch that creates host.env, so the case it was written for — a local user pre-creates the directory and plants one — was the one case it never ran in. Now unconditional and first, with an existing host.env re-owned and the inheritable OWNER RIGHTS ACE dropped (it was keeping an attacker's already-created files theirs even after the directory was re-owned).

ensure_admin_only_source is implemented. The 2026-07-05 audit recorded it as FIXED and it was in no commit in this repository's history — the local EoP it described was live, and it is the payload half of the chain above.

The identity and token readers hardened the directory only on the path that generated a new secret, so a planted cert/key or token was adopted verbatim, permanently. They harden before the first read now.

H-3 — plugin UIs get their own origin

A plugin's interface was proxied onto the console's own origin and framed allow-same-origin, so plugin JS ran as first-party code there: one fetch('/api/**', {credentials:'same-origin'}) and the BFF attached the operator's admin bearer. The "open in new tab" link was the same escalation with no iframe involved at all.

The obvious fix does not work, and that is worth knowing before reviewing this one. Dropping allow-same-origin gives the frame an opaque origin; its subresources become cross-site; the SameSite=Lax session cookie stops being sent; every plugin asset 302s to /login and the frame is blank.

So the origin moves instead — a second listener on its own port (default PORT + 1, 47993):

  • different origin (scheme+host+port) is what the same-origin policy separates on, so the plugin cannot read the console's DOM, its cross-origin fetch of /api/** is unreadable (no CORS) and cannot mutate (Sec-Fetch-Site sees same-site);
  • same site, because cookie scope ignores the port and SameSite is computed on the site — so the session cookie still reaches the plugin listener and plugin pages keep working.

Enforcement is two refusals and both are load-bearing: the console origin refuses /plugin-ui/**, and the plugin origin refuses everything else — above all /api/**, which would otherwise hand the admin bearer right back to plugin JS now same-origin with that listener. Both unconditional: if the port cannot be bound, plugin UIs are disabled and the console says so, rather than falling back to the arrangement this removes.

Two consequences that would otherwise bite in the field:

  • the new port must be open — done for the Windows netsh rule, the firewalld service and the ufw profile;
  • a browser stores a self-signed-certificate exception per origin, including the port, and an interstitial can never be shown inside an iframe — so the frame would sit blank with nothing explaining why. A no-cors probe distinguishes it (a TLS failure rejects; any HTTP answer, even 401, resolves) and the console renders a card linking the operator to open it once in a real tab.

Everything else

The three input planes are bounded and lossy like the mic plane on the same loop already was; Android's library client no longer accepts any publicly-trusted certificate for the pinned host; the usbip vhci nodes get their own group instead of riding on input, which every packaging scriptlet tells users to join; a catalog registry URL can no longer inject a TOML table into bunfig.toml; the pairing cooldown is charged before the arming state is read, so armed/disarmed is no longer a free oracle; and the whole Low tier — of which the two worth naming are a clipboard MIME NUL that panicked the host on one control message, and an unauthenticated global logout that let any LAN peer sign the operator out on a loop.

Verification

CI is green on 0d94ef0d — all four jobs, every step (rust incl. clippy/build/test/C-ABI/header-drift, rust-arm64, web, docs-site).

Locally, before pushing:

  • cargo fmt --all --check clean; cargo clippy --workspace --all-targets -- -D warnings clean.
  • cargo check -p punktfunk-host --all-targets green on Linux and Windows. The Windows green was confirmed non-vacuous — a planted type error in windows/install.rs fails the build, then removed — because two earlier runs printed Finished without ever compiling the host crate.
  • scripts/xcheck.sh windows check green (covers pf-vdisplay and pf-paths on windows-msvc).
  • cargo test -p punktfunk-host --bins: 416 passed; cargo test -p pf-clipboard: 13 passed.
  • The H-3 origin split was exercised against a running console with a fake mgmt API and a fake plugin: console /plugin-ui/** → 404; plugin-origin /api/v1/hooks, /, /login, /_auth/logout → 404; plugin page loads 200 through its own origin; unauthenticated plugin origin → 401; a forged x-pf-listener header changes nothing on either listener; the plugin's own hostile Clear-Site-Data / Access-Control-Allow-Origin / Set-Cookie are dropped by the proxy allowlist; the plugin origin's CSP names the console as its only frame-ancestors source; and with the port squatted, ui-config reports unavailable, the console still refuses /plugin-ui/**, and the console itself keeps working.

What this does not do

H-6's registry authentication, because half of it lives in unom/infra (runners/ci-core/) rather than here. The in-repo halves are done: workflow_dispatch inputs no longer interpolate into run: blocks — one of them in the step holding UPDATE_MANIFEST_KEY, the Ed25519 key every host pins — and the syft installer is pinned to its release tag instead of main. Digest pinning is deliberately left until the registry is authenticated: a tag, content-keyed or not, can simply be overwritten while anonymous pushes are accepted, so pinning first would look like a fix without being one. Both halves and their order are recorded at the top of .gitea/workflows/docker.yml.

M-5 is half done. The free armed/disarmed oracle is closed. Having the console bind the arming window to a fingerprint is not: the operator arms before any device has knocked, so there is nothing to bind to yet, and making it possible is a knock-then-bind flow change.

Before merging

H-3 wants on-glass confirmation in a real browser. The cookie and framing behaviour is reasoned from spec and verified at the HTTP level, not observed in a browser — and this changes how every plugin UI loads. Worth opening the console on a real box with a plugin installed, particularly to see that the certificate-acceptance flow reads sensibly the first time.

Two behaviour changes worth a second opinion:

  • the art proxy's default allowed root is the users base (C:\Users); a library on another drive needs PUNKTFUNK_LIBRARY_ART_ROOTS, and the failure is a 400 naming the field rather than a silently missing cover;
  • setting a launch command in the console library editor now asks for the console password, on the same reasoning as hooks — a 7-day session cookie should not be enough to leave a persistent command on the machine.
Acts on the 2026-08-05 host security review (`punktfunk-planning/design/security-review-2026-08-05.md`). 37 of its 38 findings; the one exception is called out at the bottom. Three commits, because the first two are two different arguments: - `81039581` — the plugin authorization lane, the Windows local-EoP pair, the CI substrate, and the whole Low tier. - `defdfbdb` — H-3, which needed a change of shape rather than a change of value. - `0d94ef0d` — a clippy fix CI caught in the first commit (`Result<(), Response>` → `Option<Response>`; the "error" is the response, so there was never an error value to propagate). ## The plugin lane `plugin_may_access` was the only authorization gate in the system that was **allow-by-default**: a hand-maintained denylist of route *prefixes*, where every sibling gate (`cert_may_access`, the QUIC pairing gate, the console's `isPublicPath`) is deny-by-default. Its own doc comment names the two capabilities it exists to withhold, and both were reachable one route over — not because anyone drifted, but because ~1450 commits of new routes arrived and a denylist is not a thing people remember to update. It is an allowlist now, and `every_route_is_classified_for_the_plugin_and_cert_lanes` walks the **live** route table out of `openapi_json()` and fails the build for any route not deliberately classified for both non-admin lanes. That test is the actual fix — the gate closes today's hole, the test closes the class. **Route reachability and field authority turned out to be separate questions.** A provider plugin must be able to call `PUT /library/provider/{p}` — reconciling its own entry set is what a scanner plugin *is* — but `prep` and `launch.kind == "command"` inside that payload are handed to `/bin/sh -c` (or `cmd.exe /c`) as the host user, and every execution site documents them as operator-typed. So requests now carry the lane that authorized them (`AuthLane`) and the handlers refuse those two fields to everyone but the operator's own token. A route gate cannot express that. The art proxy read any absolute path off disk **in the host process** — LocalSystem on Windows — from a path the plugin lane could write and then read back, which yielded `mgmt-token` and with it full admin. It now serves only real images (known extension **and** magic bytes, so a renamed secret fails), only from inside an allowed root, only after canonicalization, never over UNC; and a path it would refuse to serve can no longer be persisted in the first place. ## Windows The config-dir hardening was skipped exactly when it was needed: it ran only in the branch that *creates* `host.env`, so the case it was written for — a local user pre-creates the directory and plants one — was the one case it never ran in. Now unconditional and first, with an existing `host.env` re-owned and the inheritable OWNER RIGHTS ACE dropped (it was keeping an attacker's already-created files theirs even after the directory was re-owned). `ensure_admin_only_source` is implemented. The 2026-07-05 audit recorded it as FIXED and it was in **no commit in this repository's history** — the local EoP it described was live, and it is the payload half of the chain above. The identity and token readers hardened the directory only on the path that *generated* a new secret, so a planted cert/key or token was adopted verbatim, permanently. They harden before the first read now. ## H-3 — plugin UIs get their own origin A plugin's interface was proxied onto the console's own origin and framed `allow-same-origin`, so plugin JS ran as first-party code there: one `fetch('/api/**', {credentials:'same-origin'})` and the BFF attached the operator's **admin** bearer. The "open in new tab" link was the same escalation with no iframe involved at all. The obvious fix does not work, and that is worth knowing before reviewing this one. Dropping `allow-same-origin` gives the frame an **opaque** origin; its subresources become cross-site; the `SameSite=Lax` session cookie stops being sent; every plugin asset 302s to `/login` and the frame is blank. So the origin moves instead — a second listener on its own port (default `PORT + 1`, 47993): - different **origin** (scheme+host+**port**) is what the same-origin policy separates on, so the plugin cannot read the console's DOM, its cross-origin `fetch` of `/api/**` is unreadable (no CORS) and cannot mutate (`Sec-Fetch-Site` sees `same-site`); - same **site**, because cookie scope ignores the port and SameSite is computed on the site — so the session cookie still reaches the plugin listener and plugin pages keep working. Enforcement is two refusals and both are load-bearing: the console origin refuses `/plugin-ui/**`, and the plugin origin refuses everything *else* — above all `/api/**`, which would otherwise hand the admin bearer right back to plugin JS now same-origin with *that* listener. Both unconditional: if the port cannot be bound, plugin UIs are **disabled** and the console says so, rather than falling back to the arrangement this removes. Two consequences that would otherwise bite in the field: - **the new port must be open** — done for the Windows netsh rule, the firewalld service and the ufw profile; - **a browser stores a self-signed-certificate exception per origin, including the port**, and an interstitial can never be shown inside an iframe — so the frame would sit blank with nothing explaining why. A `no-cors` probe distinguishes it (a TLS failure rejects; any HTTP answer, even 401, resolves) and the console renders a card linking the operator to open it once in a real tab. ## Everything else The three input planes are bounded and lossy like the mic plane on the same loop already was; Android's library client no longer accepts any publicly-trusted certificate for the pinned host; the usbip vhci nodes get their own group instead of riding on `input`, which every packaging scriptlet tells users to join; a catalog registry URL can no longer inject a TOML table into `bunfig.toml`; the pairing cooldown is charged before the arming state is read, so armed/disarmed is no longer a free oracle; and the whole Low tier — of which the two worth naming are a clipboard MIME NUL that panicked the host on one control message, and an unauthenticated global logout that let any LAN peer sign the operator out on a loop. ## Verification CI is green on `0d94ef0d` — all four jobs, every step (`rust` incl. clippy/build/test/C-ABI/header-drift, `rust-arm64`, `web`, `docs-site`). Locally, before pushing: - `cargo fmt --all --check` clean; `cargo clippy --workspace --all-targets -- -D warnings` clean. - `cargo check -p punktfunk-host --all-targets` green on **Linux and Windows**. The Windows green was confirmed **non-vacuous** — a planted type error in `windows/install.rs` fails the build, then removed — because two earlier runs printed `Finished` without ever compiling the host crate. - `scripts/xcheck.sh windows check` green (covers pf-vdisplay and pf-paths on windows-msvc). - `cargo test -p punktfunk-host --bins`: **416 passed**; `cargo test -p pf-clipboard`: 13 passed. - The H-3 origin split was exercised against a **running console** with a fake mgmt API and a fake plugin: console `/plugin-ui/**` → 404; plugin-origin `/api/v1/hooks`, `/`, `/login`, `/_auth/logout` → 404; plugin page loads 200 through its own origin; unauthenticated plugin origin → 401; a forged `x-pf-listener` header changes nothing on either listener; the plugin's own hostile `Clear-Site-Data` / `Access-Control-Allow-Origin` / `Set-Cookie` are dropped by the proxy allowlist; the plugin origin's CSP names the console as its only `frame-ancestors` source; and with the port squatted, `ui-config` reports `unavailable`, the console still refuses `/plugin-ui/**`, and the console itself keeps working. ## What this does not do **H-6's registry authentication**, because half of it lives in `unom/infra` (`runners/ci-core/`) rather than here. The in-repo halves are done: `workflow_dispatch` inputs no longer interpolate into `run:` blocks — one of them in the step holding `UPDATE_MANIFEST_KEY`, the Ed25519 key every host pins — and the syft installer is pinned to its release tag instead of `main`. Digest pinning is deliberately left until the registry is authenticated: a tag, content-keyed or not, can simply be overwritten while anonymous pushes are accepted, so pinning first would look like a fix without being one. Both halves and their order are recorded at the top of `.gitea/workflows/docker.yml`. **M-5 is half done.** The free armed/disarmed oracle is closed. Having the console *bind* the arming window to a fingerprint is not: the operator arms before any device has knocked, so there is nothing to bind to yet, and making it possible is a knock-then-bind flow change. ## Before merging **H-3 wants on-glass confirmation in a real browser.** The cookie and framing behaviour is reasoned from spec and verified at the HTTP level, not observed in a browser — and this changes how every plugin UI loads. Worth opening the console on a real box with a plugin installed, particularly to see that the certificate-acceptance flow reads sensibly the first time. Two behaviour changes worth a second opinion: - the art proxy's default allowed root is the users base (`C:\Users`); a library on another drive needs `PUNKTFUNK_LIBRARY_ART_ROOTS`, and the failure is a 400 naming the field rather than a silently missing cover; - setting a launch command in the console library editor now asks for the console password, on the same reasoning as hooks — a 7-day session cookie should not be enough to leave a persistent command on the machine.
enricobuehler added 2 commits 2026-08-05 16:48:37 +00:00
Acts on the 2026-08-05 host security review. 36 of its 38 findings; the two
exceptions are recorded below and in the review doc.

The review's headline is that `plugin_may_access` was the one authorization
gate in the system that was allow-by-default — a hand-maintained denylist of
route prefixes, where every sibling gate is deny-by-default. Its own doc
comment names the two capabilities it exists to withhold, and both were
reachable one route over, because ~1450 commits of new routes were added and
the list was never one of the things anyone remembered to update.

So the gate is now an allowlist, and a test walks the live route table and
fails the build for any route that has not been deliberately classified for
both non-admin lanes. That test is the actual fix: it is what stops the next
route from arriving pre-authorized.

Route reachability and field authority turned out to be different questions.
A provider plugin has to be able to reconcile its own library entries — that
is what a scanner plugin IS — but `prep` and a `command` launch inside that
payload are handed to `/bin/sh -c` as the host user, and every execution site
documents them as operator-typed. Requests now carry the lane that authorized
them, and those two fields are refused to everyone but the operator's own
token.

The art proxy read any absolute path off disk in the host process, which on
Windows is LocalSystem, from a path the plugin lane could write and then read
back — so it yielded `mgmt-token`, which is full admin. It now serves only
real images (extension AND magic bytes, so a renamed secret fails), only from
inside an allowed root, only after canonicalization, and never over UNC; and
a path it would refuse to serve can no longer be persisted in the first place.

On Windows, the config-dir hardening was skipped exactly when it was needed —
it ran only in the branch that CREATES host.env, so the case it was written
for (a local user pre-created the directory and planted one) was the one case
it never ran in. It is now unconditional and first, an existing host.env is
re-owned, and the inheritable OWNER RIGHTS ACE that kept an attacker's files
theirs after the directory was re-owned is gone. The identity and token
readers were hardening the directory only on the path that GENERATED a new
secret, so a planted cert/key or token was adopted verbatim and permanently;
they harden before the first read now.

`ensure_admin_only_source` is implemented. The 2026-07-05 audit recorded it as
FIXED and it was in no commit in this repository's history — the local EoP it
described was live, and it is the payload half of the config-dir chain above.

Also: the three input planes are bounded and lossy like the mic plane on the
same loop already was; Android's library client no longer accepts any
publicly-trusted certificate for the pinned host; the usbip vhci nodes get
their own group instead of riding on `input`, which every packaging scriptlet
tells users to join; a registry URL can no longer inject a TOML table into
bunfig.toml; the pairing cooldown is charged before the arming state is read,
so armed/disarmed is no longer a free oracle; and the whole Low tier, of which
the two worth naming are a clipboard MIME NUL that panicked the host on one
control message, and an unauthenticated global logout that let any LAN peer
sign the operator out on a loop.

NOT fixed, deliberately:

  H-3 (plugin UIs framed allow-same-origin). Dropping allow-same-origin does
  not work: the document's origin goes opaque, its subresource requests are
  then cross-site, the SameSite=Lax session cookie is not sent, and every
  plugin asset 302s to /login. The "open in new tab" link is the same
  escalation with no iframe at all, so the sandbox attribute is not where this
  gets fixed either. It needs a second listener — a distinct origin that is
  still the same site — which changes the console's deploy model and wants
  on-glass validation. The mechanism and the dead end are written down at the
  iframe.

  H-6 registry authentication, whose other half lives in unom/infra. The
  in-repo halves are done: workflow_dispatch inputs no longer interpolate into
  run: blocks (one of them in the step holding UPDATE_MANIFEST_KEY), and the
  syft installer is pinned to its tag instead of main. Digest pinning is left
  until the registry is authenticated, because a tag — content-keyed or not —
  can simply be overwritten while anonymous pushes are accepted.

M-5 is half done: the oracle is closed, but binding the arming window needs
the console to learn the fingerprint first, which is a knock-then-bind flow
rather than an edit.

Verified: cargo fmt --all --check clean; cargo check --all-targets green on
Linux and on Windows (confirmed non-vacuous — a planted type error in
windows/install.rs fails the build); scripts/xcheck.sh windows check green;
cargo test -p punktfunk-host --bins 416 passed, the single failure being
gamestream::stream::tests::sender_delivers_batches, the known qemu-environmental
UDP-loopback flake that fails identically on clean main in the same container;
cargo test -p pf-clipboard 13 passed; web console typechecks.
fix(security): plugin UIs get their own origin
ci / web (pull_request) Successful in 1m2s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m4s
ci / docs-site (pull_request) Successful in 2m13s
android / android (pull_request) Successful in 3m16s
ci / rust (pull_request) Failing after 3m36s
defdfbdb58
Closes H-3 of the 2026-08-05 review, the last of its six highs. A plugin's
interface was reverse-proxied onto the console's own origin and framed with
`allow-same-origin`, so plugin JS ran as first-party code on that origin: one
`fetch('/api/**', {credentials:'same-origin'})` and the BFF attached the
operator's ADMIN bearer. That reached everything `plugin_may_access` withholds
— arm pairing, read the host PIN, approve a device, read `/hooks`. The "open
in new tab" link was the same escalation with no iframe involved at all.

The fix is not a sandbox attribute, and it is worth writing down why, because
the obvious change is the one that does not work. Dropping `allow-same-origin`
gives the frame an OPAQUE origin; its subresource requests are then cross-site;
the `SameSite=Lax` session cookie stops being sent; every plugin asset 302s to
/login and the frame is blank. Nothing about the new-tab link is helped either.

So the origin moves instead. A second listener on its own port (default
PORT + 1) serves plugin UIs and nothing else:

  different ORIGIN — scheme+host+PORT — so the same-origin policy separates the
                     plugin from the console: it cannot read the console's DOM,
                     its cross-origin fetch of /api/** is unreadable (no CORS)
                     and cannot mutate (Sec-Fetch-Site sees same-site).
  same SITE        — cookie scope ignores the port and SameSite is computed on
                     the site, so the session cookie still reaches the plugin
                     listener and plugin pages keep working.

Enforcement is two refusals and both are load-bearing: the console origin
refuses /plugin-ui/**, and the plugin origin refuses everything ELSE — above
all /api/**, which would otherwise hand the admin bearer right back to plugin
JS that is now same-origin with that listener. Both are unconditional: if the
plugin port cannot be bound, plugin UIs are DISABLED and the console says so,
rather than falling back to the arrangement this exists to remove.

Two consequences that would otherwise bite in the field:

  The port has to be open. Done for the Windows netsh rule, the firewalld
  service and the ufw profile.

  A browser stores a self-signed-certificate exception per ORIGIN, including
  the port — and a certificate interstitial can never be shown inside an
  iframe, so the frame would just sit blank with nothing on screen explaining
  why. A `no-cors` probe distinguishes it (a TLS failure rejects; any HTTP
  answer, even 401, resolves) and the console renders a card linking the
  operator to open the port once in a real tab.

Also here: the health probe moved server-side to the console origin (it used
to rely on being same-origin with the plugin), the postMessage listener now
verifies `event.origin` — a real check rather than a tautology — and
plugin-kit's `postMessage(..., "*")` is documented as load-bearing, since
narrowing it to `location.origin` would now target the plugin's own origin and
silently drop every message.

Verified against a running console with a fake mgmt API and a fake plugin:
console /plugin-ui/** → 404; plugin-origin /api/v1/hooks, /, /login,
/_auth/logout → 404; plugin page loads 200 through its own origin;
unauthenticated plugin origin → 401 (not a redirect to a /login it does not
serve); a forged x-pf-listener header changes nothing on either listener; the
plugin's own Clear-Site-Data / Access-Control-Allow-Origin / Set-Cookie are
dropped by the proxy allowlist; the plugin origin's CSP names the console as
its only frame-ancestors source; and with the port squatted, ui-config reports
`unavailable`, the console still refuses /plugin-ui/**, and the console itself
keeps working.

Still wants on-glass confirmation in a real browser — the cookie and framing
behaviour is reasoned from spec, not observed.

cargo fmt --all --check clean; cargo check -p punktfunk-host --all-targets
green on Windows; web console builds and typechecks.
enricobuehler added 1 commit 2026-08-05 17:14:31 +00:00
fix(host/mgmt): the field gate returns the refusal, not an error carrying it
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m49s
ci / docs-site (pull_request) Successful in 2m17s
ci / rust-arm64 (pull_request) Successful in 2m43s
android / android (pull_request) Successful in 3m3s
ci / rust (pull_request) Successful in 8m31s
0d94ef0dbe
`check_entry_fields` returned `Result<(), Response>`, which trips
`clippy::result_large_err` under CI's `-D warnings`: an axum `Response` is 128
bytes and it was riding in the `Err` variant.

`Option<Response>` is the shape this always wanted. There is no error value to
propagate here — the "error" IS the response the handler sends back — so `None`
means "the payload may proceed" and `Some(r)` is the refusal to return. The call
sites read the same, one word different.

Caught by CI, not by me: I ran `cargo check` and not `cargo clippy -D warnings`.
enricobuehler merged commit 4a0d0ce587 into main 2026-08-05 17:39:03 +00:00
enricobuehler deleted branch worktree-security-review-0805-fixes 2026-08-05 17:39:05 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#58