72 lines
4.8 KiB
JavaScript
72 lines
4.8 KiB
JavaScript
// P4 named mutant: API key echoed in a response, error, log or durable record.
|
|
// Real HTTP -> human capability -> broker -> page -> management client -> fake.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { request } from 'node:http';
|
|
import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fakeManagement } from '../../bus/tests/proxy-page-fixture.mjs';
|
|
import { Store } from '../../bus/src/store.mjs';
|
|
import { Broker } from '../../bus/src/broker.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { createSsoSession } from './helpers/sso-session.mjs';
|
|
const scan = root => readdirSync(root, { withFileTypes: true }).flatMap(e => e.isDirectory() ? scan(join(root, e.name)) : [readFileSync(join(root, e.name))]);
|
|
test('API-key canary reaches fake once and zero responses/logs/receipts/store files', async t => {
|
|
const canary = 'p4-canary-key';
|
|
const fake = await fakeManagement(t);
|
|
const root = mkdtempSync(join(tmpdir(), 'p4-canary-'));
|
|
const store = new Store(root);
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
|
|
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
|
|
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
|
|
const logs = [], output = [];
|
|
const auth = await createSsoSession(t, { host: 'console.example', owner: { sub: 'owner-sub', name: 'owner' } });
|
|
const server = await startServer({ port: 0, sso: auth.sso,
|
|
send: async () => ({}), proxyCall: (verb, args) => b.request(cap, { verb, args }), log: line => logs.push(line) });
|
|
t.after(() => new Promise(r => server.close(r)));
|
|
const session = await auth.signIn(server.address().port);
|
|
const headers = auth.writeHeaders(session);
|
|
const call = (path, data) => new Promise((resolve, reject) => {
|
|
const req = request({ hostname: '127.0.0.1', port: server.address().port, path, method: data ? 'POST' : 'GET',
|
|
headers }, res => {
|
|
let text = ''; res.on('data', c => text += c); res.on('end', () => { output.push(text); resolve(res.statusCode); });
|
|
}); req.on('error', reject); req.end(data ? JSON.stringify(data) : undefined);
|
|
});
|
|
// Capture process output during the operation as well as structured log
|
|
// callbacks. Even a deliberate log-leak mutant must never print its canary.
|
|
const stdout = process.stdout.write, stderr = process.stderr.write;
|
|
const capture = (chunk, ...rest) => { output.push(Buffer.from(chunk)); const cb = rest.at(-1); if (typeof cb === 'function') cb(); return true; };
|
|
let added, accounts;
|
|
try {
|
|
process.stdout.write = capture; process.stderr.write = capture;
|
|
added = await call('/api/proxy/apikey/add', { provider: 'z.ai', key: canary });
|
|
accounts = await call('/api/proxy/accounts');
|
|
} finally { process.stdout.write = stdout; process.stderr.write = stderr; }
|
|
assert.equal(added, 200); assert.equal(accounts, 200);
|
|
assert.equal(fake.state.receives, 1);
|
|
assert.equal(fake.state.groups[0].keys[0]['api-key'] === canary, true);
|
|
for (const bytes of [...output, ...logs, JSON.stringify(fake.state.receipts), ...scan(root)])
|
|
assert.equal(Buffer.from(bytes).includes(Buffer.from(canary)), false, 'credential value escaped');
|
|
});
|
|
|
|
test('OAuth URLs, state and code stay out of broker records and management receipts', async t => {
|
|
const fake = await fakeManagement(t);
|
|
const root = mkdtempSync(join(tmpdir(), 'p4-oauth-'));
|
|
const store = new Store(root);
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
|
|
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
|
|
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
|
|
const call = (verb, args = {}) => b.request(cap, { verb, args });
|
|
const login = await call('proxy.login.start', { provider: 'claude' });
|
|
const callback = 'http://localhost/callback?state=' + login.state + '&code=p4-private-code';
|
|
assert.deepEqual(await call('proxy.login.submit', { redirect_url: callback }), { status: 'wait' });
|
|
assert.deepEqual(await call('proxy.login.status', { state: login.state }), { status: 'wait' });
|
|
for (const value of [login.url, login.state, callback, 'p4-private-code']) {
|
|
assert.equal(JSON.stringify(fake.state.receipts).includes(value), false);
|
|
for (const bytes of scan(root)) assert.equal(bytes.includes(Buffer.from(value)), false);
|
|
}
|
|
});
|