Tighten security defaults for upgrades, dumps, config views, and HTTP
Security-defaults part of the code quality action plan. Closes #786 (closed). Related: #720 (client timeout), #728 (argv-exec migration owns the systemic fix; the quoting here is the narrow one).
REV: no-visual-change the only UI change is a text caption and a per-field helper text in the physical envs editor, rendered by the existing MUI components with no layout or style change; the rendered strings are asserted by PhysicalMode.test.tsx.
Upgrade image allow-list
An empty provision.upgradeImageAllowList now means the repository of the clone image: databaseContainer.dockerImage plus the clone's own image when it carries an override. That accepts exactly the image the engine derives itself. ["*"] restores the old "any repository" behavior. Breaking change of the default; README, the five example configs, and the error message say so.
validateRequestedImage now fails closed on an empty list; the handler resolves the default through allowedUpgradeRepositories before calling it.
Dump-and-restore quoting
Every engine-derived value joined into the sh -c string is shellQuoted: host, user, database name (from pg_database), table names, and the restore user. The directory-format exec path still passes bare argv elements. customOptions stay verbatim in both paths: they are operator-written option strings and quoting them would break configs that rely on literal quotes.
Physical envs
PhysicalEnvs is in the sensitive group. The YAML view masks each value while keeping the keys, and GET /admin/config returns the keys with every value replaced by ****, so the UI can list which variables are set without receiving WAL-G or pgBackRest credentials.
On save, a key posted as **** keeps its stored value, placeholders included; a masked key with no stored value is refused with an actionable error. An empty envs map keeps the stored values as well, mirroring the empty-password rule. The envs editor explains masked values and lets the operator overwrite or delete a row; leaving rows alone posts the mask back, so adding or changing other variables no longer wipes the stored credentials.
HTTP hardening
api.NewServerbuilds both engine listeners withReadHeaderTimeout10s,ReadTimeout60s,IdleTimeout120s, noWriteTimeout. A test proves the hijacked websocket outlives the read timeout (gorilla clears the deadline inUpgrade).ReadJSONcaps the body at 1 MiB and returnsErrBodyTooLargewrapping*http.MaxBytesError; decode errors no longer include the body.api.SendDecodeErrormaps it to 413 (PAYLOAD_TOO_LARGE) and everything else to 400; all 13ReadJSONcall sites use it.dblabapi.Clientsetshttp.Client.Timeoutfrom--request-timeout.DownloadArtifactuses a separate client withResponseHeaderTimeoutso a streamed body is not truncated.- The webhooks client timeout landed on master first (!1199 (merged)
newClient(): 10s request, per-stage dial/TLS/idle bounds), so the merge took that over the 30s timeout this branch had.TestNewClientBoundsEveryStageis what's left here — !1199 (merged)'s timeout test overridesclient.Timeout, so the constructor itself was uncovered.
The OpenAPI specs only carry per-endpoint error examples; the new 413 code is left for the spec-drift work in !1196 (merged).