The prior change loosened the shared _is_safe_relative_path() to permit dot-prefixed
segments. That validator also gates static serving, asset URLs and manifest paths, so
loosening it made hidden files reachable: GET /extensions/<id>/.env, /.git/config and
/.git/hooks/pre-commit return 200 — credential exposure plus a git-hook write/execute
vector.
Restore _is_safe_relative_path() to strict (reject every dot-prefixed segment) and add
a separate _is_safe_archive_member() wired only into the install/uninstall loops. It
permits a narrow allowlist of benign hidden LEAF files (.gitkeep, .gitignore,
.gitattributes, .env.example) so extensions can still ship them, while dot-directories
(.git/) and all other dotfiles (.env, .secret) stay rejected.
Adds regression coverage for both validators: traversal, NUL/backslash, dot-directories,
and the leaf allowlist edges.
_is_safe_relative_path rejected any path segment starting with '.', which
blocks hidden leaf files like .gitkeep / .env.example — not just the ./..
traversal segments. This breaks install of any extension shipping a dotfile,
including the gallery's own profile-avatars (screenshots/.gitkeep) -> "Unsafe
archive member".
Drop the over-broad startswith('.') clause; the '.'/'..' guard plus the
caller's resolved.relative_to(root) containment check already cover traversal.
Verified: .gitkeep is accepted while ../evil and a/./b stay blocked.
Fixes#6619