Skip to content

Fix NIP-05 redirect SSRF and archive-transition draft loss#492

Merged
mubarakcoded merged 3 commits into
masterfrom
mac-no-device-sweep
Jul 13, 2026
Merged

Fix NIP-05 redirect SSRF and archive-transition draft loss#492
mubarakcoded merged 3 commits into
masterfrom
mac-no-device-sweep

Conversation

@mubarakcoded

@mubarakcoded mubarakcoded commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Two self-contained fixes, each with a unit test — no on-device verification needed.

  • NIP-05 resolver redirect SSRF (Security: NIP05Resolver follows HTTP redirects with no re-validation, bypassing its own SSRF host guard (redirect-based SSRF) #448). NIP05Resolver validated only the initial .well-known/nostr.json URL against the SSRF host policy; its URLSession had no redirect delegate, so a 3xx from the well-known host was auto-followed to any host — including loopback/private/link-local addresses the up-front guard blocks — turning a contact lookup into a blind internal probe. Added NIP05RedirectPolicy (a URLSessionTaskDelegate) that re-runs RemoteImageURLPolicy.isAllowed on every redirect target and cancels the task on a disallowed one, mirroring the avatar path's CappedImageDownloadDelegate. Test drives the delegate directly: loopback, link-local, private, IPv6-loopback, and scheme-downgrade targets are blocked; a public https target is followed unchanged.
  • Archive transition drops composer drafts (Bug: applyChatRows treats an active↔archived transition as a "removed" chat, silently deleting its composer draft and thrashing its group-member cache #466). applyChatRows computed "removed" chats as the union of per-list removals, so a chat that merely moved active↔archived (absent from one "next" list, present in the other) was wrongly treated as removed and had its composer draft cleared and group-member cache invalidated. Now the removed set is (previousActive ∪ previousArchived) − (nextActive ∪ nextArchived), so only truly-gone chats are cleared. Test archives a chat holding a draft through the snapshot reload and asserts the draft survives.

Closes #448
Closes #466


CI note: this branch is cut from master, which is currently red on two pre-existing tests unrelated to this change — messageMediaDiskCacheReadDeletionMaintainsTrackedFootprint (an obsolete assertion from the #444 plaintext-hash isolation change, fixed separately) and the flaky workspaceCoalescesAndCachesSourceEpochZeroMediaReferenceResolution. Both fail on master itself; the two new tests here pass and this change adds no new failures.


Open in Stage

Summary by CodeRabbit

  • Bug Fixes

    • Improved security when following redirects during NIP-05 lookups by blocking unsafe destinations and insecure scheme downgrades.
    • Preserved conversation drafts when chats move between active and archived views.
    • Prevented chats from being incorrectly treated as removed during active/archive transitions.
  • Tests

    • Added coverage for secure redirect handling and draft preservation across chat state changes.

@stage-review

stage-review Bot commented Jul 13, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 2 individual chapters for you:

Title
1 Secure NIP-05 lookups against SSRF redirects
2 Preserve composer drafts during archive transitions
Open in Stage

Chapters generated by Stage for commit 914614e on Jul 13, 2026 12:22pm UTC.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d592da9-4a53-452d-b67d-eb863522411d

📥 Commits

Reviewing files that changed from the base of the PR and between 0efa722 and 914614e.

📒 Files selected for processing (1)
  • whitenoise-macTests/whitenoise_macTests.swift

Walkthrough

Changes

NIP-05 redirect validation

Layer / File(s) Summary
Redirect policy and validation
whitenoise-mac/Core/NIP05Resolver.swift, whitenoise-macTests/whitenoise_macTests.swift
NIP05Resolver validates each redirect through NIP05RedirectPolicy, blocking disallowed targets and testing private, local, downgrade, and public HTTPS cases.

Chat list state preservation

Layer / File(s) Summary
Combined chat removal tracking
whitenoise-mac/Core/WorkspaceState+ChatList.swift, whitenoise-macTests/whitenoise_macTests.swift
Chat removals are computed across active and archived lists, preserving composer drafts when a chat moves to archived state.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main fixes: NIP-05 redirect SSRF protection and draft preservation during archive transitions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mac-no-device-sweep

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
whitenoise-mac/Core/NIP05Resolver.swift (1)

81-101: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

No redirect hop-count cap, despite the doc comment claiming parity with CappedImageDownloadDelegate.

The comment says this "Mirrors the avatar path's CappedImageDownloadDelegate," but that delegate also caps redirect chains at 5 hops (RemoteImageLoader.swift), while NIP05RedirectPolicy only re-validates the target host on each hop and will follow an unbounded number of allowed redirects. Impact is bounded today by timeoutIntervalForRequest/timeoutIntervalForResource on the session, so this isn't exploitable beyond a resource-timeout-bounded delay, but it's a real gap versus the stated intent and the sibling implementation.

🔒 Proposed fix to mirror the hop-count cap
 final class NIP05RedirectPolicy: NSObject, URLSessionTaskDelegate {
+    private let lock = NSLock()
+    private var redirectHopCount = 0
+
     func urlSession(
         _ session: URLSession,
         task: URLSessionTask,
         willPerformHTTPRedirection response: HTTPURLResponse,
         newRequest request: URLRequest,
         completionHandler: `@escaping` (URLRequest?) -> Void
     ) {
+        let hopCount = lock.withLock {
+            redirectHopCount += 1
+            return redirectHopCount
+        }
+        if hopCount > 5 {
+            completionHandler(nil)
+            task.cancel()
+            return
+        }
         guard let url = request.url, RemoteImageURLPolicy.isAllowed(url) else {
             completionHandler(nil)
             task.cancel()
             return
         }
         completionHandler(request)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@whitenoise-mac/Core/NIP05Resolver.swift` around lines 81 - 101, Update
NIP05RedirectPolicy’s
urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:) to
track redirect hops and stop following redirects after the same five-hop cap
used by CappedImageDownloadDelegate in RemoteImageLoader.swift. Cancel or reject
requests beyond the limit while preserving the existing
RemoteImageURLPolicy.isAllowed validation for each permitted target.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@whitenoise-mac/Core/NIP05Resolver.swift`:
- Around line 81-101: Update NIP05RedirectPolicy’s
urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:) to
track redirect hops and stop following redirects after the same five-hop cap
used by CappedImageDownloadDelegate in RemoteImageLoader.swift. Cancel or reject
requests beyond the limit while preserving the existing
RemoteImageURLPolicy.isAllowed validation for each permitted target.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eabe09da-4013-4f41-badc-feda814ba04e

📥 Commits

Reviewing files that changed from the base of the PR and between c8d96d9 and 0efa722.

📒 Files selected for processing (3)
  • whitenoise-mac/Core/NIP05Resolver.swift
  • whitenoise-mac/Core/WorkspaceState+ChatList.swift
  • whitenoise-macTests/whitenoise_macTests.swift

@mubarakcoded
mubarakcoded merged commit 490ca5b into master Jul 13, 2026
3 checks passed
@mubarakcoded
mubarakcoded deleted the mac-no-device-sweep branch July 13, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant