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:
1 parent
1e18c336f1
commit
077de5455c
5 files changed
+63
-35
No files matched your search
+32
-16
@@ -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 != '.':
|
||||
|
||||
Reference in new issue
Block a user