confine editor.open/editor.save to the workspace (T-363)
The editor registry resolved buffer paths with a string join that passed absolute paths through verbatim and never normalized `..` — an unconfined read and write primitive over IPC while files.read was carefully guarded. Buffer paths now resolve through resolveUnderRootFollowingSymlinks: traversal, absolute escapes, and symlinks-out are rejected at open, and re-checked at save so a symlink swapped in under an open buffer's path can't redirect the write. D-80's extra read roots deliberately do not apply — a buffer is a write surface. Handlers map PathOutsideRoot to the same error files.read uses. Also merges a duplicate Added heading that had crept into the Unreleased changelog section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3949,3 +3949,5 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBSG6356MZJ2DCCCSBMBGM', 'status', 'in_progress', 'done', NULL, '2026-06-11 22:32:08', '2026-06-11 22:32:08', '2026-06-11 22:32:08', NULL, 'f3dbb31fb6f6f416a2d4d8ef37723316', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 22:32:29', '2026-06-11 22:32:29', '2026-06-11 22:32:29', NULL, '7b833a606da59ab523e0bb43f1753f6a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'status', 'in_progress', 'done', NULL, '2026-06-11 22:34:31', '2026-06-11 22:34:31', '2026-06-11 22:34:31', NULL, '254ae09f42b4da439a04d59d675d8f42', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 22:34:51', '2026-06-11 22:34:51', '2026-06-11 22:34:51', NULL, '14a7d3d6d6c71cae423ce12c71b6c540', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'status', 'in_progress', 'done', NULL, '2026-06-11 22:39:04', '2026-06-11 22:39:04', '2026-06-11 22:39:04', NULL, 'ad1c483fe26d5e417b2e1cd8986114f3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -4013,3 +4013,9 @@ Fix: apply the same glob filtering the search side uses before rewriting. Test:
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'search.replace silently ignores its include/exclude globs', 'lib/src/search/replace_engine.dart:124-143 — the include/exclude glob filters the user typed are accepted but never applied; replace will happily rewrite files outside the filter.
|
||||
|
||||
Fix: apply the same glob filtering the search side uses before rewriting. Test: replace with an include glob touches only matching files; exclude glob is honored.', 'done', 'high', NULL, NULL, NULL, '2026-06-11 21:55:38', '2026-06-11 22:34:31', NULL, 'e86c024d6f8a6d9cd9b7273522b54511', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'editor.open/editor.save skip path confinement entirely', 'lib/src/editor/registry.dart:215-219 returns absolute paths verbatim — no .. normalization, no path_safety call — an unconfined read AND write primitive over IPC while files.read is carefully guarded.
|
||||
|
||||
Fix: route editor.open/editor.save through path_safety like files.*. Tests: traversal and absolute-escape attempts rejected for both verbs. Longer-term the confinement should move to the dispatch layer (see the systemic ticket filed with this epic).', 'in_progress', 'high', NULL, NULL, NULL, '2026-06-11 21:55:26', '2026-06-11 22:34:51', NULL, 'ff2c4e85e11c02c88eff6f8e87348d34', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'editor.open/editor.save skip path confinement entirely', 'lib/src/editor/registry.dart:215-219 returns absolute paths verbatim — no .. normalization, no path_safety call — an unconfined read AND write primitive over IPC while files.read is carefully guarded.
|
||||
|
||||
Fix: route editor.open/editor.save through path_safety like files.*. Tests: traversal and absolute-escape attempts rejected for both verbs. Longer-term the confinement should move to the dispatch layer (see the systemic ticket filed with this epic).', 'done', 'high', NULL, NULL, NULL, '2026-06-11 21:55:26', '2026-06-11 22:39:04', NULL, '50c42cb7af7715e9ea6417212be0ebdc', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
Reference in New Issue
Block a user