harden(#4581): display-only escape symlinks must not disclose target path/metadata

Codex+Opus gate (info-leak): the display-only escape-target rows still returned the resolved
outside path (target), target-derived is_dir, and target stat size; ui.js showed the outside path
in the open-dialog. Fix: emit ONLY name/path/type/target_outside_workspace/mtime for outside rows
(no target, no size, is_dir=False); dialog uses a generic 'points outside workspace' message (dropped
{target} from external_link_open_confirm across all 13 locales). Containment was already intact (both
gates confirmed); this closes the path-disclosure. Tests updated to assert the no-leak property.
This commit is contained in:
nesquena-hermes authored and nesquena-hermes committed 2026-06-21 09:15:22 +00:00
1 parent 1e18c336f1
commit 077de5455c
5 files changed
+63 -35

No files matched your search

+32 -16
View File
@@ -1232,26 +1232,42 @@ def list_dir(workspace: Path, rel: str='.'):
target_outside_workspace = True
if _is_blocked_system_path(link_target):
return
is_dir = link_target.is_dir()
display_path = name
if rel and rel != '.':
display_path = rel + '/' + display_path
mtime_ns = lstat_result.st_mtime_ns if lstat_result is not None else None
entry = {
'name': name,
'path': display_path,
'type': 'symlink',
'target': str(link_target),
'is_dir': is_dir,
'target_outside_workspace': target_outside_workspace,
'mtime_ns': mtime_ns,
}
if not is_dir:
try:
entry['size'] = link_target.stat().st_size
except OSError:
entry['size'] = None
entries.append(entry)
if target_outside_workspace:
# #4581 hardening: a display-only escape-target symlink must NOT
# disclose where it points. Emit ONLY display-safe fields — never
# the resolved outside path, target-derived is_dir, or target size
# (the row exists to show the link is present; navigation/read
# through it stays blocked by safe_resolve_ws/open_anchored_fd).
entry = {
'name': name,
'path': display_path,
'type': 'symlink',
'is_dir': False,
'target_outside_workspace': True,
'mtime_ns': mtime_ns,
}
entries.append(entry)
else:
is_dir = link_target.is_dir()
entry = {
'name': name,
'path': display_path,
'type': 'symlink',
'target': str(link_target),
'is_dir': is_dir,
'target_outside_workspace': False,
'mtime_ns': mtime_ns,
}
if not is_dir:
try:
entry['size'] = link_target.stat().st_size
except OSError:
entry['size'] = None
entries.append(entry)
else:
entry_path = name
if rel and rel != '.':