mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(cli,studio,core): close five R5 telemetry and canary findings
- A long-lived preview cached its telemetry posture in two places (readConfig and shouldTrack). Running `telemetry disable` in another terminal left it resolving canaries and injecting the CLI id for hours. Both caches are now dropped together at a request boundary. - Studio minted and shipped a telemetry id for every render regardless of the browser profile's opt-out, and the server emitted the outcome under CLI policy, which cannot see localStorage or DNT. The browser now sends an explicit telemetryOptOut, distinct from an old client's omission. - Any non-empty HYPERFRAMES_PREVIEW_HOST disabled the DNS-rebinding guard, so even a loopback bind accepted a hostile Host. The guard now holds for loopback binds and, on a LAN bind, admits only names this machine answers on. - sunsetAfter had no reader of the current date. A scheduled workflow runs scripts/check-canary-sunset.ts weekly, so a failure lands on the rollout's owner rather than on an unrelated PR author. - The install-state seed memo outlived `rm -rf ~/.hyperframes`, resurrecting a cleared cohort. Removed; it only saved a read on a readConfig cache miss. Docs updated for the Host rule and the 100% exclusion carve-out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3f69a2c635
commit
6f0df2640b
@@ -1,3 +1,4 @@
|
||||
import { hostname, networkInterfaces } from "node:os";
|
||||
import { afterEach, describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
// CLI → Studio telemetry identity seeding (Layer 1). Verifies the server only
|
||||
@@ -253,6 +254,23 @@ describe("buildStudioHeadScriptsForHost — Host split", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Non-loopback names this machine answers to. Computed, not hardcoded: the
|
||||
* rule under test is "an address/name this host actually has", so a literal
|
||||
* like `192.168.1.10` would pass only by accident on one developer's laptop.
|
||||
*/
|
||||
function localHostCandidates(): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const entries of Object.values(networkInterfaces())) {
|
||||
for (const entry of entries ?? []) {
|
||||
if (!entry.internal && entry.family === "IPv4") names.add(entry.address);
|
||||
}
|
||||
}
|
||||
const self = hostname().split(".")[0];
|
||||
if (self !== undefined && self !== "") names.add(`${self}.local`);
|
||||
return [...names];
|
||||
}
|
||||
|
||||
describe("identityAllowed — loopback-bound vs explicitly LAN-bound", () => {
|
||||
const original = process.env["HYPERFRAMES_PREVIEW_HOST"];
|
||||
|
||||
@@ -285,16 +303,44 @@ describe("identityAllowed — loopback-bound vs explicitly LAN-bound", () => {
|
||||
});
|
||||
|
||||
// The mode this regressed: browsing your own LAN-exposed Studio lost the
|
||||
// CLI stitch entirely, so the same human became two PostHog persons.
|
||||
it.each(["0.0.0.0:3000", "192.168.1.10:3000", "my-dev-box.local:3000"])(
|
||||
// CLI stitch entirely, so the same human became two PostHog persons. The
|
||||
// names come from this machine, because that is now the actual rule —
|
||||
// a hardcoded `192.168.1.10` asserted only that the check was absent.
|
||||
it.each(["0.0.0.0:3000", ...localHostCandidates().map((n) => `${n}:3000`)])(
|
||||
"allows %s once the operator opted into LAN exposure",
|
||||
(host) => {
|
||||
expect(identityAllowed(host)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("still allows loopback in that mode", () => {
|
||||
expect(identityAllowed("localhost:3000")).toBe(true);
|
||||
// Setting the env var opted into LAN exposure, NOT into handing identity
|
||||
// to whatever name a rebinding page invents. This is the hole: the old
|
||||
// rule returned true for every one of these.
|
||||
it.each(["evil.example.com", "127.0.0.1.evil.com", "attacker.test:3000", undefined])(
|
||||
"still refuses hostile Host %s",
|
||||
(host) => {
|
||||
expect(identityAllowed(host)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a LAN address this machine does not answer on", () => {
|
||||
expect(identityAllowed("203.0.113.7:3000")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// A loopback bind exposes nothing, so the Host check stays a live rebinding
|
||||
// mitigation — previously ANY non-empty value disabled it wholesale.
|
||||
describe("bound to loopback explicitly", () => {
|
||||
beforeEach(() => {
|
||||
process.env["HYPERFRAMES_PREVIEW_HOST"] = "127.0.0.1";
|
||||
});
|
||||
|
||||
it.each(["localhost:5173", "127.0.0.1"])("still allows %s", (host) => {
|
||||
expect(identityAllowed(host)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["evil.example.com", "192.168.1.10:3000"])("still refuses %s", (host) => {
|
||||
expect(identityAllowed(host)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user