Files
gstack/lib/loopback-request.ts
T
Garry Tanandcapy-ai[bot] 6cf00aee9c Require the daemon secret and loopback Host/Origin on the design and iOS QA daemons (#4117)
* Require the daemon secret and loopback Host/Origin on the design and iOS QA daemons (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Name the changelog fragment after the branch (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Review-Finding: p1/red-team#2 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: refuse cross-site browser requests on the iOS QA loopback, show the expired page for a stale board secret, refuse requests without a Host (GST-1747)

Review-Finding: v1/outside-voice#1 reader=outside-voice
Review-Finding: v1/adversarial-native#2 reader=adversarial-native
Review-Finding: v1/adversarial-native#5 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: show the secret segment in the documented design board URL (GST-1747)

Review-Finding: v1/adversarial-native#1 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: keep the daemon token fixtures on one line so the credential scan reads them as code (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: cover the Sec-Fetch-Site refusal, the expired board page and the no-Host 403 in the changelog fragment (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-07 07:39:51 +00:00

28 lines
1.2 KiB
TypeScript

/**
* Request checks for daemons bound to a loopback port. Binding 127.0.0.1
* keeps other machines out but not other origins on this machine: a browser
* page can send requests to the port, and a page served under a hostname
* that resolves to 127.0.0.1 arrives with that hostname as Host. Serving
* only loopback Host values and same-origin (or absent) Origin values keeps
* the port to local clients that address it directly.
*/
const LOOPBACK_HOSTNAMES = ['127.0.0.1', 'localhost', '[::1]'];
function isLoopbackAuthority(authority: string, port: number): boolean {
const normalized = authority.toLowerCase();
return LOOPBACK_HOSTNAMES.some((hostname) => normalized === `${hostname}:${port}`);
}
/** Why a request to a loopback daemon on `port` must be refused, or null to serve it. */
export function loopbackRequestRefusal(
host: string | null | undefined,
origin: string | null | undefined,
port: number,
): 'host_not_loopback' | 'cross_origin' | null {
if (!host || !isLoopbackAuthority(host, port)) return 'host_not_loopback';
if (origin === null || origin === undefined) return null;
if (!origin.startsWith('http://') || !isLoopbackAuthority(origin.slice('http://'.length), port)) return 'cross_origin';
return null;
}