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:
+12
-4
@@ -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 {
|
||||
|
||||
@@ -21,9 +21,20 @@ export const SectionLogin: FC<{ next?: string }> = ({ next }) => {
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
// Full reload to the target so SSR re-runs WITH the new session cookie. Only a
|
||||
// same-origin path — reject protocol-relative/absolute URLs (open-redirect guard).
|
||||
const safe = next?.startsWith("/") && !next.startsWith("//") ? next : "/";
|
||||
// Full reload to the target so SSR re-runs WITH the new session cookie. Resolve `next`
|
||||
// against our own origin and accept it ONLY if it stays same-origin, rejecting absolute
|
||||
// and protocol-relative targets. A naive `!startsWith("//")` check misses `/\evil.com`
|
||||
// (browsers fold `\`→`/` under http(s), so it resolves to //evil.com) plus tab/newline
|
||||
// tricks; parsing closes them because it's the same parser this redirect will use.
|
||||
let safe = "/";
|
||||
try {
|
||||
const u = new URL(next ?? "/", window.location.origin);
|
||||
if (u.origin === window.location.origin) {
|
||||
safe = u.pathname + u.search + u.hash;
|
||||
}
|
||||
} catch {
|
||||
safe = "/";
|
||||
}
|
||||
window.location.href = safe;
|
||||
} catch {
|
||||
setError(true);
|
||||
|
||||
Reference in New Issue
Block a user