mirror of
https://github.com/obra/superpowers.git
synced 2026-06-14 22:59:06 +08:00
Harden brainstorm companion auth regressions
This commit is contained in:
@@ -25,6 +25,13 @@ const TEST_DIR = '/tmp/brainstorm-auth-test';
|
||||
const CONTENT_DIR = path.join(TEST_DIR, 'content');
|
||||
const TOKEN = 'testtoken-0123456789abcdef0123456789abcdef';
|
||||
const COOKIE_NAME = `brainstorm-key-${TEST_PORT}`;
|
||||
const EXPECTED_SECURITY_HEADERS = {
|
||||
'referrer-policy': 'no-referrer',
|
||||
'cache-control': 'no-store',
|
||||
'x-frame-options': 'DENY',
|
||||
'content-security-policy': "frame-ancestors 'none'",
|
||||
'cross-origin-resource-policy': 'same-origin'
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
|
||||
@@ -49,9 +56,12 @@ function get(pathname, { key, cookie } = {}) {
|
||||
}
|
||||
|
||||
// Try to open a WebSocket; resolve 'opened' or 'rejected'.
|
||||
function wsConnect({ key, cookie } = {}) {
|
||||
function wsConnect({ key, cookie, origin } = {}) {
|
||||
const url = `ws://localhost:${TEST_PORT}/` + (key !== undefined ? `?key=${key}` : '');
|
||||
const opts = cookie ? { headers: { Cookie: cookie } } : {};
|
||||
const headers = {};
|
||||
if (cookie) headers['Cookie'] = cookie;
|
||||
if (origin) headers['Origin'] = origin;
|
||||
const opts = Object.keys(headers).length ? { headers } : {};
|
||||
const ws = new WebSocket(url, opts);
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
@@ -69,6 +79,21 @@ function startServer() {
|
||||
});
|
||||
}
|
||||
|
||||
function assertSecurityHeaders(headers) {
|
||||
for (const [name, value] of Object.entries(EXPECTED_SECURITY_HEADERS)) {
|
||||
assert.strictEqual(headers[name], value, `${name} should be ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function runBootstrapScript(html, sessionStorage) {
|
||||
const match = html.match(/<script>\n([\s\S]*?)\n<\/script>/);
|
||||
assert(match, 'bootstrap response should contain a script block');
|
||||
const replacements = [];
|
||||
const location = { replace(url) { replacements.push(url); } };
|
||||
new Function('sessionStorage', 'location', match[1])(sessionStorage, location);
|
||||
return replacements;
|
||||
}
|
||||
|
||||
async function waitForServer(server) {
|
||||
let stdout = '', stderr = '';
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -120,15 +145,46 @@ async function runTests() {
|
||||
assert(/key/i.test(res.body), '403 body should mention the key');
|
||||
});
|
||||
|
||||
await test('403 responses include leak-reduction and anti-framing headers', async () => {
|
||||
const res = await get('/');
|
||||
assert.strictEqual(res.status, 403);
|
||||
assertSecurityHeaders(res.headers);
|
||||
});
|
||||
|
||||
await test('GET / with wrong key is rejected with 403', async () => {
|
||||
const res = await get('/', { key: 'wrong-token' });
|
||||
assert.strictEqual(res.status, 403);
|
||||
});
|
||||
|
||||
await test('GET / with valid key serves the screen', async () => {
|
||||
await test('GET / with wrong key and valid cookie is rejected with 403', async () => {
|
||||
const res = await get('/', { key: 'wrong-token', cookie: `${COOKIE_NAME}=${TOKEN}` });
|
||||
assert.strictEqual(res.status, 403, 'explicit wrong query key must not fall back to cookie auth');
|
||||
});
|
||||
|
||||
await test('GET / with valid query returns bootstrap instead of screen content', async () => {
|
||||
const res = await get('/', { key: TOKEN });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Secret screen'), 'should serve the screen content');
|
||||
assert(res.body.includes('sessionStorage'), 'bootstrap should store the session key in tab storage');
|
||||
assert(res.body.includes('location.replace'), 'bootstrap should navigate to the bare root URL');
|
||||
assert(!res.body.includes('Secret screen'), 'bootstrap must not serve screen HTML at the keyed URL');
|
||||
});
|
||||
|
||||
await test('bootstrap strips the key URL even when sessionStorage write fails', async () => {
|
||||
const res = await get('/', { key: TOKEN });
|
||||
assert.strictEqual(res.status, 200);
|
||||
let replacements;
|
||||
assert.doesNotThrow(() => {
|
||||
replacements = runBootstrapScript(res.body, {
|
||||
setItem() { throw new Error('storage blocked'); }
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(replacements, ['/']);
|
||||
});
|
||||
|
||||
await test('HTML responses include leak-reduction and anti-framing headers', async () => {
|
||||
const res = await get('/', { key: TOKEN });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assertSecurityHeaders(res.headers);
|
||||
});
|
||||
|
||||
await test('valid key load sets an HttpOnly SameSite=Strict cookie', async () => {
|
||||
@@ -142,7 +198,8 @@ async function runTests() {
|
||||
await test('GET / with valid cookie (no query key) serves the screen', async () => {
|
||||
const res = await get('/', { cookie: `${COOKIE_NAME}=${TOKEN}` });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Secret screen'));
|
||||
assert(res.body.includes('Secret screen'), 'cookie-authenticated bare root should serve the screen');
|
||||
assert(!res.body.includes("location.replace('/');"), 'bare screen response should not be the bootstrap page');
|
||||
});
|
||||
|
||||
console.log('\n--- HTTP /files gate ---');
|
||||
@@ -158,6 +215,12 @@ async function runTests() {
|
||||
assert(res.body.includes('secret asset'));
|
||||
});
|
||||
|
||||
await test('/files responses include leak-reduction and anti-framing headers', async () => {
|
||||
const res = await get('/files/asset.txt', { key: TOKEN });
|
||||
assert.strictEqual(res.status, 200);
|
||||
assertSecurityHeaders(res.headers);
|
||||
});
|
||||
|
||||
console.log('\n--- WebSocket gate ---');
|
||||
|
||||
await test('WS upgrade without key is rejected', async () => {
|
||||
@@ -178,6 +241,33 @@ async function runTests() {
|
||||
assert.strictEqual(outcome, 'opened');
|
||||
});
|
||||
|
||||
await test('WS upgrade with valid cookie and same-origin Origin opens', async () => {
|
||||
const { outcome, ws } = await wsConnect({
|
||||
cookie: `${COOKIE_NAME}=${TOKEN}`,
|
||||
origin: `http://localhost:${TEST_PORT}`
|
||||
});
|
||||
ws.close();
|
||||
assert.strictEqual(outcome, 'opened');
|
||||
});
|
||||
|
||||
await test('WS upgrade with valid cookie but cross-origin Origin is rejected', async () => {
|
||||
const eventsFile = path.join(TEST_DIR, 'state', 'events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
|
||||
const { outcome, ws } = await wsConnect({
|
||||
cookie: `${COOKIE_NAME}=${TOKEN}`,
|
||||
origin: 'http://localhost:9999'
|
||||
});
|
||||
if (outcome === 'opened') {
|
||||
ws.send(JSON.stringify({ type: 'choice', choice: 'attacker-injected', text: 'local attacker probe' }));
|
||||
await sleep(300);
|
||||
}
|
||||
ws.close();
|
||||
|
||||
assert.strictEqual(outcome, 'rejected', 'cross-origin browser WS must not open even with cookie');
|
||||
assert(!fs.existsSync(eventsFile), 'cross-origin WS must not write state/events');
|
||||
});
|
||||
|
||||
console.log('\n--- Robustness (A3) ---');
|
||||
|
||||
await test('null payload over an authed WS does not crash the server', async () => {
|
||||
|
||||
Reference in New Issue
Block a user