From 2a9294f60031bc7b658bab6c74ca367a24f69add Mon Sep 17 00:00:00 2001 From: be-coder-07 Date: Wed, 5 Aug 2026 13:46:42 -0500 Subject: [PATCH] fix(mosaic): bound brain migration and owner lookups --- .../src/commands/brain-owner-resolver.spec.ts | 32 +++++++++++++++ .../src/commands/brain-owner-resolver.ts | 2 + .../mosaic/src/commands/brain-store.spec.ts | 9 ++++- packages/mosaic/src/commands/brain-store.ts | 40 +++++++++++++++++-- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/packages/mosaic/src/commands/brain-owner-resolver.spec.ts b/packages/mosaic/src/commands/brain-owner-resolver.spec.ts index 89a9ee22..1370b12b 100644 --- a/packages/mosaic/src/commands/brain-owner-resolver.spec.ts +++ b/packages/mosaic/src/commands/brain-owner-resolver.spec.ts @@ -164,6 +164,38 @@ describe('provider-backed durable owner resolver', (): void => { expect(calls.every((call) => call.authorization === null)).toBe(true); }); + it('refuses provider redirects and configures a bounded no-redirect request', async (): Promise => { + const resolver = await loadResolver('MB-REQ-09 owner lookup SSRF boundary'); + const requests: RequestInit[] = []; + + const result = await resolver.resolveProviderDurableOwner( + { + estateRegistrySource: estateRegistry(), + ownerPolicySource: ownerPolicy(), + host: 'git.example.invalid', + requestedOwner: 'user:durable-owner', + }, + { + fetch: async (_input, init): Promise => { + requests.push(init ?? {}); + return new Response(JSON.stringify({ message: 'redirect' }), { + status: 302, + headers: { + 'content-type': 'application/json', + location: 'http://127.0.0.1/internal', + }, + }); + }, + absentControlName: (): string => 'generated-absent-control', + }, + ); + + expect(result).toMatchObject({ verdict: 'not-measured', reasonCode: 'owner-control-invalid' }); + expect(requests).toHaveLength(1); + expect(requests[0]?.redirect).toBe('manual'); + expect(requests[0]?.signal).toBeInstanceOf(AbortSignal); + }); + it('requires the GLPI standing remediation queue in the local estate policy', async (): Promise => { const resolver = await loadResolver('MB-REQ-09 standing process policy'); const raw = JSON.parse(ownerPolicy()) as { estates: Array> }; diff --git a/packages/mosaic/src/commands/brain-owner-resolver.ts b/packages/mosaic/src/commands/brain-owner-resolver.ts index 5c881853..d5059a26 100644 --- a/packages/mosaic/src/commands/brain-owner-resolver.ts +++ b/packages/mosaic/src/commands/brain-owner-resolver.ts @@ -109,6 +109,8 @@ async function readPublicIdentity( Accept: 'application/json', 'User-Agent': 'mosaic-brain-owner/1', }, + redirect: 'manual', + signal: AbortSignal.timeout(5_000), }); } catch { throw new Error('owner-provider-unavailable'); diff --git a/packages/mosaic/src/commands/brain-store.spec.ts b/packages/mosaic/src/commands/brain-store.spec.ts index e7c2db17..839b6679 100644 --- a/packages/mosaic/src/commands/brain-store.spec.ts +++ b/packages/mosaic/src/commands/brain-store.spec.ts @@ -672,6 +672,11 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v mkdirSync(join(laneRoot, path, '..'), { recursive: true }); writeFileSync(join(laneRoot, path), 'NESTED-SECRET-MARKER\n'); } + const embeddedSecret = join(laneRoot, 'benign-looking.md'); + writeFileSync( + embeddedSecret, + 'Authorization: Bearer NESTED-CONTENT-SECRET-MARKER-1234567890\n', + ); const plan = sut.discoverBrainMigration( { sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true }, @@ -680,9 +685,9 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v expect(plan.status).toBe('ready'); expect(plan.candidates).toHaveLength(0); - expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length); + expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length + 1); expect(plan.reported.every((entry) => /secret/i.test(entry.reason))).toBe(true); - expect(JSON.stringify(plan)).not.toContain('NESTED-SECRET-MARKER'); + expect(JSON.stringify(plan)).not.toMatch(/NESTED-(?:CONTENT-)?SECRET-MARKER/); expect(existsSync(brainRoot)).toBe(false); }); diff --git a/packages/mosaic/src/commands/brain-store.ts b/packages/mosaic/src/commands/brain-store.ts index f4f9012b..78e5d253 100644 --- a/packages/mosaic/src/commands/brain-store.ts +++ b/packages/mosaic/src/commands/brain-store.ts @@ -46,6 +46,7 @@ const GITIGNORE_RULES = [ 'id_ed25519', ] as const; const BRAIN_DIRECTORIES = ['agents', 'lanes', 'board', 'specs', 'methods', 'archives'] as const; +const MAX_MIGRATION_FILE_BYTES = 1024 * 1024; const TERMINAL_EXITS = { ok: 0, refused: 10, @@ -615,6 +616,7 @@ function migrationCandidate( lane: string, ): MigrationCandidate { const source = stableSourceSnapshot(path); + if (sourceContainsSecretMaterial(source.content)) throw new Error('migration-secret-content'); const key = relative(sourceRoot, path).split(sep).join('/'); const digest = createHash('sha256') .update(key) @@ -639,6 +641,7 @@ function migrationCandidate( function secretShapedName(value: string): boolean { const name = value.toLowerCase(); return ( + ['.npmrc', '.netrc', 'auth.json', '.dockerconfigjson'].includes(name) || name === '.env' || name.startsWith('.env.') || name === 'credentials.json' || @@ -658,6 +661,23 @@ function secretShapedPath(sourceRoot: string, path: string): boolean { return relative(sourceRoot, path).split(sep).filter(Boolean).some(secretShapedName); } +function sourceContainsSecretMaterial(content: Buffer): boolean { + if (content.byteLength > MAX_MIGRATION_FILE_BYTES || content.includes(0)) return true; + let source: string; + try { + source = new TextDecoder('utf-8', { fatal: true }).decode(content); + } catch { + return true; + } + return [ + /-----BEGIN [^-\r\n]*PRIVATE KEY-----/i, + /\bauthorization\s*:\s*(?:bearer|basic)\s+\S+/i, + /\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*\S+/i, + /https?:\/\/[^/\s:@]+:[^/\s@]+@/i, + /\b[A-Za-z0-9+/_=-]{40,}\b/, + ].some((pattern): boolean => pattern.test(source)); +} + function reportAll(paths: readonly string[], reason: string): MigrationReport[] { return paths.map((path: string): MigrationReport => ({ path, reason })); } @@ -729,8 +749,14 @@ export function discoverBrainMigration( candidates.push( migrationCandidate(input.sourceRoot, input.brainRoot, path, 'lane', seat, lane), ); - } catch { - reported.push({ path, reason: 'Lane state was not a regular file; retained and reported.' }); + } catch (error: unknown) { + reported.push({ + path, + reason: + error instanceof Error && error.message === 'migration-secret-content' + ? 'Content may contain secret material; retained and reported.' + : 'Lane state was not a regular file; retained and reported.', + }); } } for (const path of seatFiles) { @@ -745,8 +771,14 @@ export function discoverBrainMigration( candidates.push( migrationCandidate(input.sourceRoot, input.brainRoot, path, 'seat', seat, lane), ); - } catch { - reported.push({ path, reason: 'Seat state was not a regular file; retained and reported.' }); + } catch (error: unknown) { + reported.push({ + path, + reason: + error instanceof Error && error.message === 'migration-secret-content' + ? 'Content may contain secret material; retained and reported.' + : 'Seat state was not a regular file; retained and reported.', + }); } } return { status: 'ready', candidates, reported, owner: ownerResolution.principal };