v2.172.0
Security — the uptime check's redirect-hop SSRF guard had the same check-then-reconnect gap v2.166.0 closed for the initial request
v2.166.0 closed a DNS-rebind TOCTOU at 9 SSRF-guarded call sites by pinning
each connection to the exact address resolve_validated had just approved —
except the uptime HTTP check's redirect-following, which was tracked
separately as harder: it still re-validated each hop with a synchronous,
blocking check (host_resolves_internal_blocking) and then let reqwest
resolve the hop's hostname AGAIN, independently, to make the actual
connection. A DNS server that answered those two lookups differently — a
rebind, or simply a low TTL between the redirect-policy decision and the
connect — sailed through unseen, on any redirect a monitored URL could send.
Closed by installing helpers::ValidatingResolver, a reqwest::dns::Resolve
implementation, as the uptime check's client-wide DNS resolver. It shares its
internal-address classifier (resolve_all_validated) with resolve_validated
rather than duplicating the rule, so validation and resolution are now the
SAME lookup for every hop, not two — there is nothing left to race. The
existing synchronous per-hop check stays in place alongside it: a redirect
Location that is already a literal internal IP never reaches a Resolve
implementation at all (the connector dials a literal IP directly), so that
check is still the only thing that catches that shape.
Tests
ssrf-url-guard-pin-e2e.sh gains §E (8 → 12): the resolver is wired into the
uptime client's builder, its own resolve() calls the real validator rather
than a stub, the pre-existing synchronous check is still present alongside it
(not replaced by it), and the validator is the shared core for both the
initial request and every redirect hop rather than a second, divergable copy.
Mutation-tested against the pre-fix file: 3 of the 4 new arms went red, and the
"synchronous check still present" arm correctly stayed green in both states,
confirming it isolates the ADDITION rather than testing something already
true. Three new Rust unit tests exercise ValidatingResolver through the
actual reqwest::dns::Resolve trait method on its literal-IP branch (no
network I/O, so no CI flake risk) — the DNS-dependent branch is exercised live
by the uptime check itself, same as resolve_validated/pinned_client,
neither of which has a dedicated unit test for the same reason.
Security — the public status page could serve one tenant's data to a visitor of another's
GET /api/status-page/public (unauthenticated, what /status fetches) picks
one status_page_config row to answer as, then read every other table it
needs — status_page_components, the monitors joined into them,
managed_incidents, incident_updates, and the legacy monitor-based
incidents table — with no tenant filter at all. On a single-tenant box
this is invisible: the one config row is the only data there is to leak. On
any multi-tenant or reseller install — DockPanel's stated target market, with
teams, resellers and reseller_dashboard all built for exactly that — a
second tenant's components and incident history would be served on the
FIRST tenant's public page the moment that second tenant configured and
enabled a status page of their own. Found by the s418 audit's setup critic,
who traced it to a finding already on record from s251 (2026-07-25) that
survived an adjacent edit (v2.70.0) to the very same function without being
closed. All five queries in public_status_page are now scoped to the
config-winning row's user_id; when no tenant has configured a status page
at all, the handler now answers with empty components/incidents instead of
whatever happened to be in the tables. Regression-pinned:
status-page-gate-pin-e2e.sh §G, mutation-tested both directions (reverting
any one of the five queries to its unscoped form turns its own arm red, and
only that one).
Security — OAuth login could be redirected into an attacker's account (login-CSRF)
/api/auth/oauth/{provider}/callback validated its state parameter only
against the server-side map authorize had written it into — which proves
some /authorize call produced it, nothing about which browser is
presenting it now. An attacker could start their own OAuth flow, capture the
provider's redirect back to this panel (a real code+state pair for the
attacker's own account) without letting their own browser follow it, and hand
that exact callback URL to a victim: the old check would accept it and log
the victim's browser into the attacker's account. authorize now sets a
short-lived HttpOnly, SameSite=Lax cookie carrying the same state value;
callback requires it to be present and to match query.state before
anything else runs.
Separately, an OAuth provider's own email_verified: false was never
checked before its email was used to auto-link to (or auto-log into) an
existing local account by email match alone. Google's OIDC userinfo endpoint
(the one this panel calls) does return that field; it was read nowhere.
Absent-field providers (GitLab's user endpoint, GitHub's profile-email path)
are unaffected — the gate defaults to allow when the field isn't sent, and
rejects only an explicit false.
Both found by the s418 identity/access-control audit fan-out (finder +
independent adversarial skeptic, both UPHELD against source). Regression-pinned
in a new suite, oauth-csrf-pin-e2e.sh, mutation-tested both directions.
Changed — an advertised capability withdrawn
Recorded in FEATURES.md § Withdrawn Claims rather than quietly deleted.
Terraform/Pulumi IaC tokens — advertised since Tier 3 as "scoped tokens,
resource listing" — are CRUD-only and authenticate nothing. All 6 handlers
(token create/list/delete, tf_list_sites, tf_list_databases, and the 4
autoscale handlers sharing this file) take only AuthUser (JWT);
iac_tokens.token_hash is written once at creation and read back by no code
path anywhere in the tree — confirmed live against demo.dockpanel.dev, a
real token 401s identically to a garbage string. The module's own doc
comment claimed "Authentication via IaC tokens (Bearer token) or regular
JWT" and named dns_records as a resource; neither was ever true — no
dns_records reader has ever existed, only sites and databases are
real. There is no frontend for it at all: the only Terraform/Pulumi/iac
hit in the whole SPA is a command-palette search keyword. iac_tokens has
zero rows on this box — zero real-world adoption, ever. Same defect shape as
the API Keys withdrawal below (write-only credential), but this one had
never been disclosed anywhere until found by the s418 audit fan-out's
completeness critic, checking the one file none of the session's four picked
audit topics had covered.
Install: curl -sL dockpanel.dev/install.sh | bash
Every release is installed on a throwaway VPS and driven as a user before
it ships. What that means, and what it has found, is written up at
https://docs.dockpanel.dev/testing.html.
Binaries are static musl builds signed with Sigstore; checksums.txt
and the .sig/.pem pairs are attached below.
Full changelog: v2.170.0...v2.172.0