diff --git a/scripts/lib/gap-candidate-ledger.ts b/scripts/lib/gap-candidate-ledger.ts index abce4646f8..cc8468648a 100644 --- a/scripts/lib/gap-candidate-ledger.ts +++ b/scripts/lib/gap-candidate-ledger.ts @@ -924,8 +924,6 @@ function writeExclusiveLedger( writeAll(fd, raw); fsyncSync(fd); temporaryStatus = fstatSync(fd); - closeSync(fd); - fd = undefined; beforeReplace?.(); assertLeaseActive(lease); assertSameRegularInode( @@ -948,6 +946,8 @@ function writeExclusiveLedger( "Gap candidate ledger readback did not match initial bytes", ); } + closeSync(fd); + fd = undefined; } catch (error) { if (errorCode(error) === "EEXIST") { throw new GapCandidateLedgerError( @@ -985,8 +985,6 @@ function replaceLedger( writeAll(fd, nextRaw); fsyncSync(fd); temporaryStatus = fstatSync(fd); - closeSync(fd); - fd = undefined; beforeReplace?.(); assertLeaseActive(lease); @@ -1018,6 +1016,8 @@ function replaceLedger( "Gap candidate ledger readback did not match replacement bytes", ); } + closeSync(fd); + fd = undefined; } finally { if (fd !== undefined) closeSync(fd); try { diff --git a/scripts/lib/git-principle-publication-adapter.ts b/scripts/lib/git-principle-publication-adapter.ts index 9eb197946d..721fa85c6a 100644 --- a/scripts/lib/git-principle-publication-adapter.ts +++ b/scripts/lib/git-principle-publication-adapter.ts @@ -462,6 +462,21 @@ function matchingEntry(parent: string, segment: string): string | undefined { return matches[0]; } +function canonicalChildDirectory( + parent: string, + segment: string, + label: string, +): string { + const existing = matchingEntry(parent, segment); + if (existing === undefined) { + throw new Error(`${label} is absent: ${join(parent, segment)}`); + } + if (existing !== segment) { + throw new Error(`${label} path is case-colliding: ${join(parent, existing)}`); + } + return realDirectory(join(parent, existing), label); +} + function safeSegments(path: string, label: string): string[] { const segments = path.split("/"); if ( @@ -603,14 +618,11 @@ function authoritativeCorpusEntries( repoRoot: string, excludedRepoPaths: ReadonlySet, ): Array<{ path: string; bytes: Buffer }> { - const knowledgeBaseRoot = realDirectory( - join(repoRoot, "knowledge-base"), + const knowledgeBaseRoot = canonicalChildDirectory( + repoRoot, + "knowledge-base", "Principle publication knowledge base", ); - const knowledgeBaseEntry = matchingEntry(repoRoot, "knowledge-base"); - if (knowledgeBaseEntry !== "knowledge-base") { - throw new Error("Principle publication knowledge-base path is case-colliding"); - } const entries: Array<{ path: string; bytes: Buffer }> = []; const walk = (directory: string, relativePath: string): void => { @@ -796,8 +808,9 @@ function assertRequestIdentity( throw new Error("Git adapter attempt nonce must be a SHA-256 value"); } assertReviewedPrinciplePlanSourceRun( - realDirectory( - join(repoRoot, "knowledge-base"), + canonicalChildDirectory( + repoRoot, + "knowledge-base", "Publication knowledge base", ), intent.reviewed_plan, diff --git a/scripts/lib/ingest/safe-fetch.test.ts b/scripts/lib/ingest/safe-fetch.test.ts index 9e7fa2d704..798643629b 100644 --- a/scripts/lib/ingest/safe-fetch.test.ts +++ b/scripts/lib/ingest/safe-fetch.test.ts @@ -21,6 +21,7 @@ describe("safe remote fetch boundary", () => { "172.16.0.1", "192.168.1.1", "192.0.2.1", + "198.18.0.1", "224.0.0.1", "::1", "::ffff:7f00:1", @@ -28,14 +29,26 @@ describe("safe remote fetch boundary", () => { "fe80::1", "fec0::1", "64:ff9b:1::a00:1", + "100::1", + "100:0:0:1::1", + "2001:2::1", + "2001:5::1", + "2001:10::1", "::ffff:0:7f00:1", "2001:db8::1", + "3fff::1", + "5f00::1", "ff02::1", ]) { expect(isPublicIpAddress(address), address).toBe(false); } expect(isPublicIpAddress("8.8.8.8")).toBe(true); expect(isPublicIpAddress("2606:4700:4700::1111")).toBe(true); + expect(isPublicIpAddress("2001:1::1")).toBe(true); + expect(isPublicIpAddress("2001:3::1")).toBe(true); + expect(isPublicIpAddress("2001:4:112::1")).toBe(true); + expect(isPublicIpAddress("2001:20::1")).toBe(true); + expect(isPublicIpAddress("2001:30::1")).toBe(true); }); it("rejects unsafe schemes, credentials, and local hostnames before fetch", async () => { diff --git a/scripts/lib/ingest/safe-fetch.ts b/scripts/lib/ingest/safe-fetch.ts index ae3d5c073c..a5b839914f 100644 --- a/scripts/lib/ingest/safe-fetch.ts +++ b/scripts/lib/ingest/safe-fetch.ts @@ -11,6 +11,27 @@ import { import { parseCredentialFreeHttpUrl } from "./url-policy"; const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]); +const ADDITIONAL_NON_PUBLIC_IPV4_RANGES = [ + ipaddr.IPv4.parseCIDR("198.18.0.0/15"), +]; +const GLOBALLY_REACHABLE_IPV6_SPECIAL_RANGES = [ + ipaddr.IPv6.parseCIDR("2001:1::1/128"), + ipaddr.IPv6.parseCIDR("2001:1::2/128"), + ipaddr.IPv6.parseCIDR("2001:1::3/128"), + ipaddr.IPv6.parseCIDR("2001:3::/32"), + ipaddr.IPv6.parseCIDR("2001:4:112::/48"), + ipaddr.IPv6.parseCIDR("2001:20::/28"), + ipaddr.IPv6.parseCIDR("2001:30::/28"), +]; +const ADDITIONAL_NON_PUBLIC_IPV6_RANGES = [ + ipaddr.IPv6.parseCIDR("fec0::/10"), + ipaddr.IPv6.parseCIDR("64:ff9b:1::/48"), + ipaddr.IPv6.parseCIDR("100::/64"), + ipaddr.IPv6.parseCIDR("100:0:0:1::/64"), + ipaddr.IPv6.parseCIDR("2001::/23"), + ipaddr.IPv6.parseCIDR("3fff::/20"), + ipaddr.IPv6.parseCIDR("5f00::/16"), +]; export interface SafeFetchDependencies { assertSafeUrl(input: string | URL): Promise; @@ -91,7 +112,22 @@ const safeDispatcher = new Agent({ export function isPublicIpAddress(address: string): boolean { if (!ipaddr.isValid(address)) return false; - return ipaddr.process(address).range() === "unicast"; + const parsed = ipaddr.process(address); + if ( + parsed instanceof ipaddr.IPv6 && + GLOBALLY_REACHABLE_IPV6_SPECIAL_RANGES.some((range) => parsed.match(range)) + ) { + return true; + } + if (parsed.range() !== "unicast") return false; + if (parsed instanceof ipaddr.IPv4) { + return !ADDITIONAL_NON_PUBLIC_IPV4_RANGES.some((range) => + parsed.match(range), + ); + } + return !ADDITIONAL_NON_PUBLIC_IPV6_RANGES.some((range) => + parsed.match(range), + ); } function isLocalHostname(hostname: string): boolean {