Skip to content

Sandbox.Denial: clean description, don't leak host paths#4

Merged
odrobnik merged 1 commit into
mainfrom
fix/denial-description-no-leak
May 13, 2026
Merged

Sandbox.Denial: clean description, don't leak host paths#4
odrobnik merged 1 commit into
mainfrom
fix/denial-description-no-leak

Conversation

@odrobnik
Copy link
Copy Markdown
Contributor

Summary

  • Sandbox.Denial is the error Shell.authorize(_:) throws when policy rejects a URL. It carries an implementer-defined suggestion: URL? hint that, in practice, embeds the embedder's host sandbox root (the factory builds it as hintRoot.appendingPathComponent(...) in Sandbox+Factories.swift).
  • ArgumentParser's fullMessage(for:) falls through to String(describing: error) for unrecognised error types. For a plain struct that's a reflective dump of every stored property — including the host-path-bearing suggestion. Embedders running SwiftPorts CLIs (gh, glab, git, …) as in-process bash builtins (Cocoanetics/iBash uses this exact shape) end up printing the iOS app container path to the user's terminal on a single denied call:
    Error: Denial(url: file:///Users/.../Containers/.../Documents/Foo.bar,
                  reason: "file URL is outside sandbox root",
                  suggestion: Optional(file:///Users/.../home))
    
  • Add CustomStringConvertible + LocalizedError conformances that surface only reason. Callers that genuinely want the URLs still read .url and .suggestion directly.

Test plan

  • swift test (33/33 passing)
  • New denialDescriptionDoesNotLeakUrls regression — build a Denial whose url/suggestion both point at /secret/host/root/... and assert that "\(denial)", String(describing: denial), and denial.localizedDescription all contain the reason but neither the host path, the literal suggestion field name, nor the Denial( struct shape.

🤖 Generated with Claude Code

`Sandbox.Denial` is thrown by `Shell.authorize(_:)` when policy
rejects a URL. The struct carries `url`, `reason`, and an
implementer-defined `suggestion: URL?` hint — the suggestion
typically encodes "where this URL would have landed under the
first sandbox root", which means it embeds the embedder's host
sandbox root.

For an iOS-app-as-sandbox embedder (Cocoanetics/iBash) that's a
direct PII-shaped leak. ArgumentParser's `fullMessage(for:)`
falls through to `String(describing: error)` for unrecognised
error types, and Swift's default reflective dump for a plain
struct prints every stored property:

    Error: Denial(
        url: file:///Users/.../Containers/.../Documents/Foo.bar,
        reason: "file URL is outside sandbox root",
        suggestion: Optional(file:///Users/.../home))

A SwiftPorts CLI like `gh auth login`, registered as a builtin
in an in-process bash, would print that to the user's terminal
on a single denied call.

Add `CustomStringConvertible` + `LocalizedError` conformances
that return only `reason`. Same surface for `"\(denial)"`,
`String(describing: denial)`, and `denial.localizedDescription`.
Callers that want the URLs read `.url` and `.suggestion`
directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@odrobnik odrobnik merged commit ea43630 into main May 13, 2026
5 checks passed
@odrobnik odrobnik deleted the fix/denial-description-no-leak branch May 13, 2026 19:39
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