Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/skills/coc-knowledge/references/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ CoC server exposes HTTP endpoints organized by domain. All routes are registered
|--------|------|-------------|
| GET | `/api/fs/browse` | Browse local directories for repo path selection |
| GET | `/api/fs/browse-helper` | Same-origin helper page for container-mode directory browsing |
| GET | `/api/fs/blob?path=<absolute>` | Read a single file when the absolute path is under CoC trusted data directories or inside any registered workspace/repo root; rejects arbitrary filesystem paths |
| GET | `/api/fs/blob?path=<absolute>` | Read a single file when the absolute path is under CoC trusted data directories (`~/.copilot`, the server data dir, or the OS temp dir) or inside any registered workspace/repo root; rejects arbitrary filesystem paths |

## Git

Expand Down
3 changes: 3 additions & 0 deletions packages/coc/src/server/tasks/tasks-handler-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import type { TasksViewerSettings, TaskFolder } from '@plusplusoneplusplus/forge
*/
export const TRUSTED_READ_ONLY_DIRS: string[] = [
path.join(os.homedir(), '.copilot'),
// The OS temp directory holds tool-output and other transient files the
// dashboard needs to read back (e.g. copilot-tool-output-*.txt).
os.tmpdir(),
];

/** Return true when `target` is inside any of the trusted read-only directories or the server data directory. */
Expand Down
29 changes: 23 additions & 6 deletions packages/coc/test/server/api-fs-blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,29 @@ describe('GET /api/fs/blob', () => {

it('returns 403 for paths outside trusted directories', async () => {
await startServer();
const outsidePath = path.join(os.tmpdir(), 'not-trusted.txt');
// Use a directory under the home folder (but not ~/.copilot) so it is
// genuinely outside all trusted roots, including the OS temp dir.
const outsideDir = path.join(os.homedir(), '_test_fs_blob_untrusted_' + Date.now());
fs.mkdirSync(outsideDir, { recursive: true });
cleanupDirs.push(outsideDir);
const outsidePath = path.join(outsideDir, 'not-trusted.txt');
fs.writeFileSync(outsidePath, 'secret');
const { status, body } = await apiGet(`/api/fs/blob?path=${encodeURIComponent(outsidePath)}`);
expect(status).toBe(403);
expect(body.error).toContain('outside trusted directories');
});

it('accepts paths within the OS temp directory', async () => {
await startServer();
const filePath = path.join(os.tmpdir(), 'copilot-tool-output-' + Date.now() + '.txt');
fs.writeFileSync(filePath, 'tool output');
try {
const { status, body } = await apiGet(`/api/fs/blob?path=${encodeURIComponent(outsidePath)}`);
expect(status).toBe(403);
expect(body.error).toContain('outside trusted directories');
const { status, body } = await apiGet(`/api/fs/blob?path=${encodeURIComponent(filePath)}`);
expect(status).toBe(200);
expect(body.content).toBe('tool output');
expect(body.encoding).toBe('utf-8');
} finally {
fs.unlinkSync(outsidePath);
fs.unlinkSync(filePath);
}
});

Expand Down Expand Up @@ -175,7 +190,9 @@ describe('GET /api/fs/blob', () => {
});

it('rejects sibling paths outside registered workspaces', async () => {
const parentDir = fs.mkdtempSync(path.join(os.tmpdir(), '_test_fs_blob_parent_'));
// Place the workspace under the home folder (not the OS temp dir, which
// is now trusted) so the sibling is genuinely outside all trusted roots.
const parentDir = fs.mkdtempSync(path.join(os.homedir(), '_test_fs_blob_parent_'));
cleanupDirs.push(parentDir);
const repoDir = path.join(parentDir, 'repo');
const siblingDir = path.join(parentDir, 'repo-sibling');
Expand Down
3 changes: 2 additions & 1 deletion packages/coc/test/server/image-preview-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ describe('Image Preview API', () => {
const srv = await startServer();
await registerWorkspace(srv, workspaceDir);

const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'));
// Use home dir (not ~/.copilot, not os.tmpdir) — genuinely outside all trusted roots.
const outsideDir = fs.mkdtempSync(path.join(os.homedir(), '_test_img_outside_'));
const outsidePath = path.join(outsideDir, 'secret.svg');
fs.writeFileSync(outsidePath, '<svg></svg>');

Expand Down
3 changes: 2 additions & 1 deletion packages/coc/test/server/tasks-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ describe('Tasks Handler', () => {

it('should reject paths outside workspace and task root', async () => {
const srv = await startServer();
const evilDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evil-'));
// Use home dir (not ~/.copilot, not os.tmpdir) — genuinely outside all trusted roots.
const evilDir = fs.mkdtempSync(path.join(os.homedir(), '_test_tasks_evil_'));
fs.writeFileSync(path.join(evilDir, 'secret.txt'), 'secret', 'utf-8');
try {
const wsId = await registerWorkspace(srv, workspaceDir);
Expand Down
Loading