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
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.
31 lines
2.1 KiB
XML
31 lines
2.1 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!--
|
|
firewalld service definition for the punktfunk management web console (the optional punktfunk-web
|
|
package: device pairing, status, GPU selection, performance graphs).
|
|
|
|
Installed to /usr/lib/firewalld/services/ by the punktfunk-host package. NOT enabled automatically
|
|
(packages never touch the admin's firewall). Only useful if you installed the console (punktfunk-web)
|
|
AND want to reach it from another device on the LAN — the console binds all interfaces on TCP 47992
|
|
(HTTPS, login-gated), and serves plugin UIs from a SEPARATE ORIGIN on TCP 47993 (see below).
|
|
The streaming host itself does not need this open; enable it deliberately with
|
|
firewall-cmd (add-service=punktfunk-web, then reload). CachyOS/Ubuntu: use the ufw punktfunk-web
|
|
profile instead.
|
|
|
|
The mgmt REST API (TCP 47990) is a different surface — the console proxies to it over loopback, and
|
|
it is opened for paired clients by the punktfunk-native profile (not here). Off-loopback the mgmt API
|
|
serves only read-only status/library to a paired client cert; its admin surface stays loopback-only.
|
|
-->
|
|
<service>
|
|
<short>Punktfunk web console</short>
|
|
<description>The optional punktfunk management web console (device pairing, status, GPU selection, performance graphs) over HTTPS. Open only if you run the punktfunk-web package and want the console reachable from other devices on the LAN.</description>
|
|
<port protocol="tcp" port="47992"/> <!-- HTTPS web console (login-gated) -->
|
|
<!--
|
|
Plugin UIs, on their OWN ORIGIN. Not a second console: a plugin's interface is third-party code,
|
|
and serving it on the console's origin let it act as the logged-in operator (security-review
|
|
2026-08-05 H-3). Same host, same certificate, different port — a different origin to the browser,
|
|
but still same-site, so the session cookie reaches it. Login-gated exactly like the console.
|
|
Only needed if you use plugins that ship a UI and want to reach them from another device.
|
|
-->
|
|
<port protocol="tcp" port="47993"/> <!-- HTTPS plugin UIs (login-gated, separate origin) -->
|
|
</service>
|