Skip to content

ref(security): Add SafeDirectory to enforce path traversal checks#623

Open
runningcode wants to merge 2 commits into
mainfrom
no/safe-directory
Open

ref(security): Add SafeDirectory to enforce path traversal checks#623
runningcode wants to merge 2 commits into
mainfrom
no/safe-directory

Conversation

@runningcode
Copy link
Copy Markdown
Contributor

@runningcode runningcode commented May 15, 2026

Summary

This PR makes the safe path the default path by introducing SafeDirectory — a Path-like wrapper that validates untrusted input stays within bounds.

  • ZipProvider.extract_to_temp_directory() returns SafeDirectory instead of Path
  • SafeDirectory.resolve(untrusted) replaces the manual is_safe_path() + raise UnsafePathError pattern — developers can't accidentally skip the check
  • SafeDirectory.child(untrusted) creates a scoped subdirectory (e.g. AAB's base/ dir)
  • Trusted Path operations (glob, rglob, /, exists, relative_to, etc.) are delegated directly, so SafeDirectory is a drop-in wherever Path was used before
  • Icon parsers (IconParser, BinaryXmlDrawableParser, ProtoXmlDrawableParser) now accept only SafeDirectory, enforcing the safety boundary at the type level
  • UnsafePathError / UnreasonableZipError moved to exceptions.py to break a circular import

🤖 Generated with Claude Code

ZipProvider.extract_to_temp_directory() now returns a SafeDirectory
instead of a raw Path. SafeDirectory.resolve(untrusted) validates that
attacker-controlled paths (manifest values, plist entries) stay within
the extraction directory, replacing the manual is_safe_path() + raise
pattern at each call site. This makes the safe path the default path
— developers can't accidentally skip the check when joining untrusted
input to an extract directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry Bot commented May 15, 2026

📲 Install Builds

iOS

🔗 App Name App ID Version Configuration
HackerNews com.emergetools.hackernews 3.8 (1) Release

Android

🔗 App Name App ID Version Configuration
Hacker News com.emergetools.hackernews 1.0.2 (13) Release

⚙️ launchpad-test-ios Build Distribution Settings

SafeDirectory now delegates glob, rglob, iterdir, /, exists, is_dir,
relative_to, __fspath__, and common properties (name, stem, suffix,
parent) to the underlying Path. This eliminates the .path accessor
from all call sites.

Icon parsers (IconParser, BinaryXmlDrawableParser,
ProtoXmlDrawableParser) now accept only SafeDirectory instead of
Path | SafeDirectory, enforcing the safety boundary at the type level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@runningcode runningcode marked this pull request as ready for review May 15, 2026 15:00
@@ -0,0 +1,10 @@
class UnreasonableZipError(ValueError):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from zipprovider to avoid circular imports.

@runningcode runningcode requested a review from chromy May 15, 2026 15:03
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8a90e9f. Configure here.

for icon_name in icon_info.primary_icon_files:
if not is_safe_path(app_bundle_path, icon_name):
raise UnsafePathError(f"Unsafe icon name in plist: {icon_name}")
app_bundle.resolve(icon_name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discarded resolve result silently performs validation

Medium Severity

app_bundle.resolve(icon_name) is called as a bare expression with its return value silently discarded. It serves as the sole path traversal validation for untrusted plist input, but it looks identical to dead code. Every other call site in this PR assigns the result (e.g., icon_path = self._extract_dir.resolve(icon_path_str) in apk.py and aab.py), making this the only place where the critical security check is invisible. A future developer or automated cleanup could remove this "unused" expression, silently disabling the path traversal protection for icon names.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8a90e9f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant