fix(container): confine outbound attachment paths

This commit is contained in:
hinotoi-agent
2026-04-25 11:38:12 +08:00
committed by gavrielc
parent 212281ba8e
commit 852009dcb1
2 changed files with 110 additions and 7 deletions

View File

@@ -23,6 +23,8 @@ import {
sessionDir,
inboundDbPath,
outboundDbPath,
readOutboxFiles,
clearOutbox,
} from './session-manager.js';
import { getSession, findSession } from './db/sessions.js';
import type { InboundEvent } from './channels/adapter.js';
@@ -108,6 +110,58 @@ describe('session manager', () => {
outDb.close();
});
it('should reject outbound attachment filenames that escape the message outbox', () => {
initSessionFolder('ag-1', 'sess-test');
const dir = sessionDir('ag-1', 'sess-test');
const msgOutbox = path.join(dir, 'outbox', 'msg-1');
fs.mkdirSync(msgOutbox, { recursive: true });
const outside = path.join(TEST_DIR, 'outside.txt');
fs.writeFileSync(outside, 'outside secret');
expect(readOutboxFiles('ag-1', 'sess-test', 'msg-1', ['../../../../../outside.txt'])).toBeUndefined();
});
it('should reject outbound attachment symlinks that escape the message outbox', () => {
initSessionFolder('ag-1', 'sess-test');
const dir = sessionDir('ag-1', 'sess-test');
const msgOutbox = path.join(dir, 'outbox', 'msg-1');
fs.mkdirSync(msgOutbox, { recursive: true });
const outside = path.join(TEST_DIR, 'outside.txt');
fs.writeFileSync(outside, 'outside secret');
fs.symlinkSync('../../../../../outside.txt', path.join(msgOutbox, 'safe-name.txt'));
expect(readOutboxFiles('ag-1', 'sess-test', 'msg-1', ['safe-name.txt'])).toBeUndefined();
});
it('should not recursively delete outside the outbox for unsafe message ids', () => {
initSessionFolder('ag-1', 'sess-test');
const victimDir = path.join(TEST_DIR, 'victim-dir');
fs.mkdirSync(victimDir, { recursive: true });
fs.writeFileSync(path.join(victimDir, 'keep.txt'), 'do not delete');
clearOutbox('ag-1', 'sess-test', '../../../../victim-dir');
expect(fs.existsSync(path.join(victimDir, 'keep.txt'))).toBe(true);
});
it('should still read and clear normal basename outbox files', () => {
initSessionFolder('ag-1', 'sess-test');
const dir = sessionDir('ag-1', 'sess-test');
const msgOutbox = path.join(dir, 'outbox', 'msg-1');
fs.mkdirSync(msgOutbox, { recursive: true });
fs.writeFileSync(path.join(msgOutbox, 'result.txt'), 'ok');
const files = readOutboxFiles('ag-1', 'sess-test', 'msg-1', ['result.txt']);
expect(files).toHaveLength(1);
expect(files?.[0]?.filename).toBe('result.txt');
expect(files?.[0]?.data.toString()).toBe('ok');
clearOutbox('ag-1', 'sess-test', 'msg-1');
expect(fs.existsSync(msgOutbox)).toBe(false);
});
it('should resolve to existing session (shared mode)', () => {
const { session: s1, created: c1 } = resolveSession('ag-1', 'mg-1', null, 'shared');
expect(c1).toBe(true);