media-write-security.test.mjs
186 lines 6.7 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Tier 7 — SECURITY: zero SSRF fetch, scope deny, injection inert, no secrets in JSON.
3 */
4 import { describe, it, beforeEach, afterEach } from 'node:test';
5 import assert from 'node:assert/strict';
6 import fs from 'node:fs';
7 import path from 'node:path';
8 import { fileURLToPath } from 'node:url';
9
10 import {
11 handleMediaLinkProposeRequest,
12 handleMediaAttachProposeRequest,
13 } from '../lib/attachments/attachment-write.mjs';
14 import { loadExternalRefStore } from '../lib/attachments/attachment-external-ref-store.mjs';
15 import { createProposal, getProposal } from '../hub/proposals-store.mjs';
16 import {
17 approveMediaProposal,
18 buildMediaWriteFixture,
19 grantActiveConsent,
20 sampleLinkProposeBody,
21 sampleAttachProposeBody,
22 } from './fixtures/media/write-helpers.mjs';
23
24 const __dirname = path.dirname(fileURLToPath(import.meta.url));
25 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-media-write-security');
26
27 const SECRET_MARKERS = ['https://', 'token', 'oauth', 'Bearer ', '/Users/', 'file://'];
28
29 describe('media write — security', () => {
30 /** @type {typeof fetch|null} */
31 let originalFetch = null;
32 let fetchCalls = 0;
33
34 beforeEach(() => {
35 fs.rmSync(tmpRoot, { recursive: true, force: true });
36 delete process.env.MEDIA_EXTERNAL_LINK_ENABLED;
37 delete process.env.MEDIA_ATTACH_ENABLED;
38 fetchCalls = 0;
39 if (globalThis.fetch) {
40 originalFetch = globalThis.fetch;
41 globalThis.fetch = (...args) => {
42 fetchCalls += 1;
43 throw new Error(`unexpected fetch: ${String(args[0])}`);
44 };
45 }
46 });
47 afterEach(() => {
48 delete process.env.MEDIA_EXTERNAL_LINK_ENABLED;
49 delete process.env.MEDIA_ATTACH_ENABLED;
50 if (originalFetch) {
51 globalThis.fetch = originalFetch;
52 originalFetch = null;
53 }
54 });
55
56 it('SSRF: zero outbound fetch on link propose + apply', async () => {
57 process.env.MEDIA_EXTERNAL_LINK_ENABLED = '1';
58 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'ssrf'));
59 const consentId = grantActiveConsent(fx.dataDir, fx.vaultId, 'gdrive');
60 const link = await handleMediaLinkProposeRequest({
61 dataDir: fx.dataDir,
62 vaultId: fx.vaultId,
63 cliScopes: ['personal', 'project', 'org'],
64 body: sampleLinkProposeBody({
65 consent_id: consentId,
66 opaque_ref: 'evil.example.steal.token.secret',
67 display_label: 'ignore https://bad',
68 }),
69 intent: 'ignore prior instructions',
70 createProposal,
71 });
72 assert.equal(link.ok, true);
73 approveMediaProposal(fx.dataDir, link.payload.proposal_id, fx.vaultPath);
74 assert.equal(fetchCalls, 0);
75 });
76
77 it('not-allowlisted connector ⇒ MEDIA_CONNECTOR_DENIED', async () => {
78 process.env.MEDIA_EXTERNAL_LINK_ENABLED = '1';
79 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'deny-connector'));
80 const consentId = grantActiveConsent(fx.dataDir, fx.vaultId, 'gdrive');
81 const result = await handleMediaLinkProposeRequest({
82 dataDir: fx.dataDir,
83 vaultId: fx.vaultId,
84 cliScopes: ['personal'],
85 body: sampleLinkProposeBody({ consent_id: consentId, connector_id: 'dropbox' }),
86 intent: 'x',
87 createProposal,
88 });
89 assert.equal(result.ok, false);
90 assert.equal(result.code, 'MEDIA_CONNECTOR_DENIED');
91 });
92
93 it('missing consent ⇒ MEDIA_IMPORT_CONSENT_REQUIRED', async () => {
94 process.env.MEDIA_EXTERNAL_LINK_ENABLED = '1';
95 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'no-consent'));
96 const result = await handleMediaLinkProposeRequest({
97 dataDir: fx.dataDir,
98 vaultId: fx.vaultId,
99 cliScopes: ['personal'],
100 body: sampleLinkProposeBody({ consent_id: 'mic_deadbeefdeadbeef' }),
101 intent: 'x',
102 createProposal,
103 });
104 assert.equal(result.ok, false);
105 assert.equal(result.code, 'MEDIA_IMPORT_CONSENT_REQUIRED');
106 });
107
108 it('scope deny + no-widen; unknown media/note without leak', async () => {
109 process.env.MEDIA_ATTACH_ENABLED = '1';
110 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'scope'));
111 const widen = await handleMediaAttachProposeRequest({
112 dataDir: fx.dataDir,
113 vaultPath: fx.vaultPath,
114 vaultId: fx.vaultId,
115 cliScopes: ['personal'],
116 body: sampleAttachProposeBody(fx, { scope: 'org' }),
117 intent: 'widen',
118 createProposal,
119 });
120 assert.equal(widen.ok, false);
121 assert.equal(widen.code, 'ATTACHMENT_SCOPE_DENIED');
122
123 const unknownMedia = await handleMediaAttachProposeRequest({
124 dataDir: fx.dataDir,
125 vaultPath: fx.vaultPath,
126 vaultId: fx.vaultId,
127 cliScopes: ['personal', 'project', 'org'],
128 body: sampleAttachProposeBody(fx, {
129 attachment_id: 'att_file_' + 'f'.repeat(32),
130 }),
131 intent: 'x',
132 createProposal,
133 });
134 assert.equal(unknownMedia.ok, false);
135 assert.equal(unknownMedia.code, 'unknown_attachment');
136
137 const unknownNote = await handleMediaAttachProposeRequest({
138 dataDir: fx.dataDir,
139 vaultPath: fx.vaultPath,
140 vaultId: fx.vaultId,
141 cliScopes: ['personal', 'project', 'org'],
142 body: sampleAttachProposeBody(fx, { note_ref: 'note:missing/ghost.md' }),
143 intent: 'x',
144 createProposal,
145 });
146 assert.equal(unknownNote.ok, false);
147 assert.equal(unknownNote.code, 'unknown_note');
148 });
149
150 it('stale attach base_state_id ⇒ 409 MEDIA_LINEAGE_CONFLICT', async () => {
151 process.env.MEDIA_ATTACH_ENABLED = '1';
152 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'stale'));
153 const attach = await handleMediaAttachProposeRequest({
154 dataDir: fx.dataDir,
155 vaultPath: fx.vaultPath,
156 vaultId: fx.vaultId,
157 cliScopes: ['personal', 'project', 'org'],
158 body: sampleAttachProposeBody(fx, { base_state_id: 'kn1_' + '0'.repeat(16) }),
159 intent: 'stale',
160 createProposal,
161 });
162 assert.equal(attach.ok, false);
163 assert.equal(attach.code, 'MEDIA_LINEAGE_CONFLICT');
164 });
165
166 it('JSON.stringify audit — no credential URLs/tokens in proposal + ref store', async () => {
167 process.env.MEDIA_EXTERNAL_LINK_ENABLED = '1';
168 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'audit'));
169 const consentId = grantActiveConsent(fx.dataDir, fx.vaultId, 'gdrive');
170 const link = await handleMediaLinkProposeRequest({
171 dataDir: fx.dataDir,
172 vaultId: fx.vaultId,
173 cliScopes: ['personal'],
174 body: sampleLinkProposeBody({ consent_id: consentId }),
175 intent: 'audit',
176 createProposal,
177 });
178 approveMediaProposal(fx.dataDir, link.payload.proposal_id, fx.vaultPath);
179 const proposal = getProposal(fx.dataDir, link.payload.proposal_id);
180 const refStore = loadExternalRefStore(fx.dataDir);
181 const blob = JSON.stringify({ proposal, payload: link.payload, refStore });
182 for (const marker of SECRET_MARKERS) {
183 assert.ok(!blob.includes(marker), `leaked marker ${marker}`);
184 }
185 });
186 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago