[iOS / XMPP Links] External pre-authorized invitation links silently add mutual contacts and expose presence and device IP #18

Open
opened 2026-11-04 20:52:10 +00:00 by asemeniv · 1 comment
Member

[iOS / XMPP Links] External pre-authorized invitation links silently add mutual contacts and expose presence and device IP

Severity: Critical
Issue ID: INV-APP, INV-02, INV-04
Date: November 4, 2026

Summary

When AnotherIM opens an external XMPP invitation link (xmpp:…?roster;preauth=…), it immediately accepts the invitation without displaying a confirmation dialog.

A single tap on a link from Safari, another application, or a QR code can therefore establish a mutual contact relationship without explicit user approval.

Once added, the link creator can:

  • See the recipient's online presence.
  • Trigger automatic acceptance of incoming P2P file transfers, exposing the recipient's network IP address through Jingle signaling (P2P-06).
  • Trigger automatic attachment downloads under the default contact autofetch policy.

The issue originates in the iOS link handler introduced in build 94. libtacky performs the requested roster accept_invite operation as designed.

Testing confirmed:

  • Pre-authorized invitations establish mutual roster subscriptions.
  • The inviter receives the recipient's presence.
  • An incoming JET file transfer is automatically accepted and exposes the recipient's IP address.
  • The iOS external-link handler invokes invitation acceptance without user confirmation.

Important: The invitation and subsequent effects were reproduced in the test environment. Opening external links on a physical iPhone has not yet been tested.

Affected components:

  • iOS: AnotherIM/Shell/AppShell.swift, XMPPLink.swift
  • libtacky: tacky/xmpp/modules/roster.tcl, jinglefile.tcl, file.tcl

Environment

Item Details
Application anotherim-swift 8f62071, branch feat/ui-redesign, build 94
libtacky 2f42a2f2 / v1.2.0-beta2
Test server ejabberd 26.09, anotherim-server b9e675d
Recipient main@example.local, libtacky application profile
Inviter inv@example.local, initially absent from recipient's roster
Recipient IP 192.0.2.2 (test environment)
Verification Level 1 cloud-based test and build 94 source-code review

Preconditions

  1. main and inv are not contacts.

  2. inv generates a pre-authorized invitation using roster invite, equivalent to Add Contact → Invite with a Link.

  3. The generated URI has the following format:

    xmpp:inv@example.local?roster;preauth=<token>

Steps to Reproduce

Run:

SWIFT_SRC=<anotherim-swift-build-94> SERVER_SRC=<anotherim-server> bash setup/run_all.sh run_invite

  1. INV-01: Generate an invitation using roster invite.
  2. INV-02: Accept the URI as main using roster accept_invite -uri <URI>, matching the operation invoked by the iOS external-link handler.
  3. INV-03: Inspect both server rosters and the presence stanzas delivered to inv.
  4. INV-04: Have inv send a 3,000-byte JET file over Jingle/SOCKS5. Do not perform any action on main.
  5. INV-APP: Inspect the build 94 external-link handling implementation in AppShell.swift.

Expected Behavior

Before accepting an external invitation, the application should display the inviter's JID and require explicit confirmation, with an option to cancel.

This would match the existing behavior for:

  • Ordinary ?roster links, which open the Add Contact sheet.
  • ?join links, which display a confirmation dialog.

XEP-0379 v0.3.3 §3.4 describes a roster-addition dialog allowing the user to edit or cancel, while §5.4 recommends displaying the JID and proposed display name.

This is a security recommendation based on XEP-0379, not an explicitly documented product requirement.

Actual Behavior

INV-APP — No confirmation in the iOS link handler

AppShell.swift:124-125:

case .invitation:
    if let jid = try? await clients.compose.acceptInvite(account, url.absoluteString) {
        request = .open(ChatID(account: account, jid: jid))
    }

The invitation is accepted immediately without presenting a confirmation sheet or dialog.

The application also selects the first configured account (AppShell.swift:118-120) without asking the user.

INV-02 — Mutual contact relationship established

Run 23351bd8 (evidence/stanzas_invite_23351bd8.log) recorded:

  • Line 223: main sends <presence type='subscribe'> containing the pre-authorization token.
  • Line 242: main sends <presence type='subscribed'> without an additional user action.
  • Server roster inspection confirms both / both subscriptions.

Neither account had the other in its roster before invitation acceptance.

INV-03 — Presence disclosure

Line 246 confirms that inv receives the recipient's available presence after the mutual subscription is established.

INV-04 — Automatic file acceptance and IP disclosure

The inviter initiated a JET file transfer:

  • Line 284, 14:20:30 UTC: session-initiate.
  • Line 314, 14:20:31 UTC: session-accept containing:
<candidate host='192.0.2.2'
           type='direct'
           .../>

The recipient's events immediately transitioned from active to done, without an offered state requiring user confirmation.

This reproduces the IP disclosure mechanism documented in P2P-06.

Reproduction Rate

2/2 successful automated runs:

  • 23351bd8
  • c393904b

Both runs confirmed mutual subscription, presence disclosure, and automatic Jingle acceptance with the recipient's direct IP candidate.

Root Cause

The external-link handler treats a pre-authorized invitation as already approved by the user.

  1. XMPPLink.swift:47 classifies ?roster and ?subscribe links containing preauth as .invitation.
  2. AppShell.swift:124-125 immediately invokes acceptInvite without confirmation.
  3. libtacky roster.tcl:805-821 adds the inviter to the roster, sends a subscription request with the token, and pre-authorizes the reciprocal subscription.
  4. Once the inviter becomes a contact, libtacky's default contact-based autofetch and Jingle acceptance rules apply:
    • jinglefile.tcl:1058-1059, 1075-1080
    • file.tcl:158-159, 681-687
  5. With direct P2P candidates enabled, the Jingle response includes the recipient's network address (P2P-06).

The underlying libtacky invitation behavior is intentional for an invitation the user has chosen to accept. The issue is that the iOS external-link handler skips that decision.

Related behavior: The in-app Add Contact → Scan Code flow also accepts invitations immediately (ComposeModels.swift:189-205), but it is initiated explicitly from the contact-addition screen and includes a warning about mutual contact creation. This report concerns externally opened links only.

Suggested Fix

  1. Require confirmation for external invitations. Display the existing Add Contact sheet with the inviter's JID, optional display name, and Add / Cancel actions.
  2. Call acceptInvite only after the user selects Add.
  3. Support account selection when multiple accounts are configured instead of automatically using the first account.

The existing confirmation behavior for ordinary roster and group invitation links can serve as a reference.

Verification After Fix

  1. Generate a pre-authorized invitation from an account not present in the recipient's roster.
  2. Open it externally through Safari, another application, or a QR code.
  3. Verify that the application displays the inviter's JID and requires confirmation.
  4. Select Cancel and verify:
    • No roster entry is created.
    • No subscription or presence is sent to the inviter.
    • The inviter cannot trigger contact-only automatic file acceptance.
  5. Repeat and select Add. Verify that the invitation is accepted and the expected mutual contact relationship is established.
  6. Repeat with multiple configured accounts and verify that the user can select the intended account.
  7. Test on a physical iPhone running build 94.

Evidence and Status

Evidence:

  • evidence/stanzas_invite_23351bd8.txt
  • evidence/stanzas_invite_c393904b.txt
  • evidence/stanzas_invite_<run>.log
  • evidence/events_main_<run>
  • evidence/events_inv_<run>
  • evidence/invite_<run>.json
  • evidence/AppShell.swift_100-130.txt
  • evidence/XMPPLink.swift.txt
  • evidence/roster.tcl_805-821.txt
  • evidence/jinglefile.tcl_1052-1083.txt

Automated test:

autotests/metadata-audit/checks/run_invite.py

Test report:

results/2026-11-04_invite_ephemeral.md, section 2.

Complete evidence archive:

_claude_tmp/evidence_invite_ephemeral_2026-11-04.tgz

Verified:

  • Mutual roster subscription without an additional confirmation step in the tested invitation flow.
  • Presence disclosure to the inviter.
  • Automatic JET file acceptance and direct IP candidate disclosure.
  • Reproduction in two automated runs.
  • Relevant source code in anotherim-swift 8f62071 and libtacky 2f42a2f2.
  • Relevant guidance in XEP-0379 §3.4 and §5.4.

Not verified:

  • End-to-end external-link handling on a physical iPhone.
  • Whether tapping an XMPP link inside a chat reaches onOpenURL.
  • iOS system prompts when opening links from Safari.
  • Automatic acceptance of files larger than 5 MiB; source code indicates these require user interaction.
# [iOS / XMPP Links] External pre-authorized invitation links silently add mutual contacts and expose presence and device IP **Severity:** Critical **Issue ID:** INV-APP, INV-02, INV-04 **Date:** November 4, 2026 ## Summary When AnotherIM opens an external XMPP invitation link (`xmpp:…?roster;preauth=…`), it immediately accepts the invitation without displaying a confirmation dialog. A single tap on a link from Safari, another application, or a QR code can therefore establish a mutual contact relationship without explicit user approval. Once added, the link creator can: - See the recipient's online presence. - Trigger automatic acceptance of incoming P2P file transfers, exposing the recipient's network IP address through Jingle signaling (P2P-06). - Trigger automatic attachment downloads under the default contact autofetch policy. The issue originates in the iOS link handler introduced in build 94. libtacky performs the requested `roster accept_invite` operation as designed. **Testing confirmed:** - Pre-authorized invitations establish mutual roster subscriptions. - The inviter receives the recipient's presence. - An incoming JET file transfer is automatically accepted and exposes the recipient's IP address. - The iOS external-link handler invokes invitation acceptance without user confirmation. **Important:** The invitation and subsequent effects were reproduced in the test environment. Opening external links on a physical iPhone has not yet been tested. **Affected components:** - iOS: `AnotherIM/Shell/AppShell.swift`, `XMPPLink.swift` - libtacky: `tacky/xmpp/modules/roster.tcl`, `jinglefile.tcl`, `file.tcl` ## Environment | Item | Details | |---|---| | Application | anotherim-swift `8f62071`, branch `feat/ui-redesign`, build 94 | | libtacky | `2f42a2f2` / v1.2.0-beta2 | | Test server | ejabberd 26.09, anotherim-server `b9e675d` | | Recipient | `main@example.local`, libtacky application profile | | Inviter | `inv@example.local`, initially absent from recipient's roster | | Recipient IP | `192.0.2.2` (test environment) | | Verification | Level 1 cloud-based test and build 94 source-code review | ## Preconditions 1. `main` and `inv` are not contacts. 2. `inv` generates a pre-authorized invitation using `roster invite`, equivalent to **Add Contact → Invite with a Link**. 3. The generated URI has the following format: `xmpp:inv@example.local?roster;preauth=<token>` ## Steps to Reproduce Run: `SWIFT_SRC=<anotherim-swift-build-94> SERVER_SRC=<anotherim-server> bash setup/run_all.sh run_invite` 1. **INV-01:** Generate an invitation using `roster invite`. 2. **INV-02:** Accept the URI as `main` using `roster accept_invite -uri <URI>`, matching the operation invoked by the iOS external-link handler. 3. **INV-03:** Inspect both server rosters and the presence stanzas delivered to `inv`. 4. **INV-04:** Have `inv` send a 3,000-byte JET file over Jingle/SOCKS5. Do not perform any action on `main`. 5. **INV-APP:** Inspect the build 94 external-link handling implementation in `AppShell.swift`. ## Expected Behavior Before accepting an external invitation, the application should display the inviter's JID and require explicit confirmation, with an option to cancel. This would match the existing behavior for: - Ordinary `?roster` links, which open the **Add Contact** sheet. - `?join` links, which display a confirmation dialog. XEP-0379 v0.3.3 §3.4 describes a roster-addition dialog allowing the user to edit or cancel, while §5.4 recommends displaying the JID and proposed display name. This is a security recommendation based on XEP-0379, not an explicitly documented product requirement. ## Actual Behavior **INV-APP — No confirmation in the iOS link handler** `AppShell.swift:124-125`: ```swift case .invitation: if let jid = try? await clients.compose.acceptInvite(account, url.absoluteString) { request = .open(ChatID(account: account, jid: jid)) } ``` The invitation is accepted immediately without presenting a confirmation sheet or dialog. The application also selects the first configured account (`AppShell.swift:118-120`) without asking the user. **INV-02 — Mutual contact relationship established** Run `23351bd8` (`evidence/stanzas_invite_23351bd8.log`) recorded: - Line 223: `main` sends `<presence type='subscribe'>` containing the pre-authorization token. - Line 242: `main` sends `<presence type='subscribed'>` without an additional user action. - Server roster inspection confirms `both` / `both` subscriptions. Neither account had the other in its roster before invitation acceptance. **INV-03 — Presence disclosure** Line 246 confirms that `inv` receives the recipient's available presence after the mutual subscription is established. **INV-04 — Automatic file acceptance and IP disclosure** The inviter initiated a JET file transfer: - Line 284, 14:20:30 UTC: `session-initiate`. - Line 314, 14:20:31 UTC: `session-accept` containing: ```xml <candidate host='192.0.2.2' type='direct' .../> ``` The recipient's events immediately transitioned from `active` to `done`, without an `offered` state requiring user confirmation. This reproduces the IP disclosure mechanism documented in P2P-06. ## Reproduction Rate **2/2 successful automated runs:** - `23351bd8` - `c393904b` Both runs confirmed mutual subscription, presence disclosure, and automatic Jingle acceptance with the recipient's direct IP candidate. ## Root Cause The external-link handler treats a pre-authorized invitation as already approved by the user. 1. `XMPPLink.swift:47` classifies `?roster` and `?subscribe` links containing `preauth` as `.invitation`. 2. `AppShell.swift:124-125` immediately invokes `acceptInvite` without confirmation. 3. libtacky `roster.tcl:805-821` adds the inviter to the roster, sends a subscription request with the token, and pre-authorizes the reciprocal subscription. 4. Once the inviter becomes a contact, libtacky's default contact-based autofetch and Jingle acceptance rules apply: - `jinglefile.tcl:1058-1059, 1075-1080` - `file.tcl:158-159, 681-687` 5. With direct P2P candidates enabled, the Jingle response includes the recipient's network address (P2P-06). The underlying libtacky invitation behavior is intentional for an invitation the user has chosen to accept. The issue is that the iOS external-link handler skips that decision. **Related behavior:** The in-app **Add Contact → Scan Code** flow also accepts invitations immediately (`ComposeModels.swift:189-205`), but it is initiated explicitly from the contact-addition screen and includes a warning about mutual contact creation. This report concerns externally opened links only. ## Suggested Fix 1. **Require confirmation for external invitations.** Display the existing **Add Contact** sheet with the inviter's JID, optional display name, and **Add / Cancel** actions. 2. Call `acceptInvite` only after the user selects **Add**. 3. **Support account selection** when multiple accounts are configured instead of automatically using the first account. The existing confirmation behavior for ordinary roster and group invitation links can serve as a reference. ## Verification After Fix 1. Generate a pre-authorized invitation from an account not present in the recipient's roster. 2. Open it externally through Safari, another application, or a QR code. 3. Verify that the application displays the inviter's JID and requires confirmation. 4. Select **Cancel** and verify: - No roster entry is created. - No subscription or presence is sent to the inviter. - The inviter cannot trigger contact-only automatic file acceptance. 5. Repeat and select **Add**. Verify that the invitation is accepted and the expected mutual contact relationship is established. 6. Repeat with multiple configured accounts and verify that the user can select the intended account. 7. Test on a physical iPhone running build 94. ## Evidence and Status Evidence: - `evidence/stanzas_invite_23351bd8.txt` - `evidence/stanzas_invite_c393904b.txt` - `evidence/stanzas_invite_<run>.log` - `evidence/events_main_<run>` - `evidence/events_inv_<run>` - `evidence/invite_<run>.json` - `evidence/AppShell.swift_100-130.txt` - `evidence/XMPPLink.swift.txt` - `evidence/roster.tcl_805-821.txt` - `evidence/jinglefile.tcl_1052-1083.txt` Automated test: `autotests/metadata-audit/checks/run_invite.py` Test report: `results/2026-11-04_invite_ephemeral.md`, section 2. Complete evidence archive: `_claude_tmp/evidence_invite_ephemeral_2026-11-04.tgz` **Verified:** - Mutual roster subscription without an additional confirmation step in the tested invitation flow. - Presence disclosure to the inviter. - Automatic JET file acceptance and direct IP candidate disclosure. - Reproduction in two automated runs. - Relevant source code in anotherim-swift `8f62071` and libtacky `2f42a2f2`. - Relevant guidance in XEP-0379 §3.4 and §5.4. **Not verified:** - End-to-end external-link handling on a physical iPhone. - Whether tapping an XMPP link inside a chat reaches `onOpenURL`. - iOS system prompts when opening links from Safari. - Automatic acceptance of files larger than 5 MiB; source code indicates these require user interaction.
Contributor
  • #18: Fixed in 4832765: an external ?roster;preauth= link asks "Add X to your contacts?" with Cancel/Add, and the invitation is accepted only after Add. The account is still the first one; choosing among several remains open.
- #18: Fixed in 4832765: an external ?roster;preauth= link asks "Add X to your contacts?" with Cancel/Add, and the invitation is accepted only after Add. The account is still the first one; choosing among several remains open.
Sign in to join this conversation.
No labels
done
fixed
returned
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
another-im/anotherim-swift#18
No description provided.