fix(security): anti-replay, 0600 client key, open redirect, supply-chain

Address findings from a repo security review:

- core: add a sliding-window anti-replay filter over the AEAD-authenticated
  sequence in Session (poll_input/poll_frame), closing the input-replay gap the
  data plane previously left to the LAN/VPN trust assumption. 4096-deep window,
  unit-tested; the encrypted loopback suite confirms no false drops.
- clients: write the mTLS client private key 0600 and lock the config dir 0700
  on Unix (it was world-readable at the umask default), re-locking existing
  stores on load. pf-client-core::trust plus the probe's own identity writer.
  Windows keeps the %APPDATA% ACL; Android/Apple already wrap the key.
- web: fix a post-login open redirect — resolve `next` via URL and require it to
  stay same-origin, rejecting `/\evil.com` and tab/encoding variants the old
  `!startsWith("//")` guard missed. Also fixes the dead safeNextPath helper.
- ci: SHA-256-pin the BtbN FFmpeg DLLs bundled into the signed Windows installer
  (were fetched from the rolling `latest` tag unverified); fails closed on a
  re-roll, matching the VB-CABLE gate.
- ci: fail-open fork-guard on the Windows/Apple host-mode PR build jobs that
  share runner labels with the signing jobs. Definitive fix stays server-side
  (Gitea outside-collaborator approval / isolated PR runners) — see the notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 10:04:23 +02:00
parent ef39050dbc
commit e707a962b6
9 changed files with 338 additions and 18 deletions
+12 -4
View File
@@ -121,11 +121,19 @@ export function isPublicPath(pathname: string): boolean {
return false;
}
/** Validate a post-login redirect target: a same-origin path only. Rejects protocol-
* relative (`//evil.com`) and absolute URLs to prevent an open redirect. */
/** Validate a post-login redirect target: a same-origin path only. Resolves `next` against a
* sentinel origin and keeps it only if it stays same-origin — rejecting absolute (`https://evil.com`),
* protocol-relative (`//evil.com`) AND backslash/tab variants (`/\evil.com`, which the WHATWG URL
* parser folds to `//evil.com`) that a plain `startsWith("//")` guard lets through. */
export function safeNextPath(next: string | undefined): string {
if (!next?.startsWith("/") || next.startsWith("//")) return "/";
return next;
if (!next) return "/";
try {
const base = "http://pf.invalid";
const u = new URL(next, base);
return u.origin === base ? u.pathname + u.search + u.hash : "/";
} catch {
return "/";
}
}
export interface SessionData {