Skip to content

fix(desktop): one duplicate-param policy for deep links - #6080

Open
Chessing234 wants to merge 4 commits into
block:mainfrom
Chessing234:fix/deep-link-param-policy
Open

fix(desktop): one duplicate-param policy for deep links#6080
Chessing234 wants to merge 4 commits into
block:mainfrom
Chessing234:fix/deep-link-param-policy

Conversation

@Chessing234

Copy link
Copy Markdown
Contributor

deep_link.rs carries four query-param parsers with three different policies for a repeated parameter:

parser repeated param
parse_entity_deep_link refuses the URL
parse_websocket_relay_param, optional_non_empty_param takes the first
parse_message_deep_link, parse_join_deep_link assigned in a loop, so takes the last

The in-app parsers take the first — messageLink.ts reads searchParams.get, entityLink.ts refuses outright. So the same URL routes one way when clicked inside the app and another when handed to the app by the OS, and buzz://join?relay=…&code=abc&code=evil displays one code while carrying another.

single_param is now the one reader — absent, empty and repeated all resolve to None — and every query-param parser goes through it. Refusing rather than picking is the right call here because our own builders never emit a duplicate, so any URL with one is malformed by construction.

Second commit: the message parser now validates what its neighbours validate. parse_channel_deep_link, twenty lines up, parses the channel UUID and requires the message id to be 64 hex characters, lowercasing it. The query form of the same two identifiers accepted anything non-empty, so a malformed link produced a payload the frontend could never route, and an id differing only in case looked like a different message.

Four existing tests change, and I want that visible. They used placeholder values — channel=abc&id=xyz, thread=root1 — and now use a real UUID and real event ids. They assert which params are required, not that malformed values are accepted; the parser beside them has always refused those exact strings. thread= empty still reads as absent, as before; present-but-malformed is now a rejection.

New coverage: three repeated-param cases for message (second channel, id, thread), two for join (second code, second relay), and the shape cases. The join pair is the one worth having.

Not changed: unknown params are still ignored on the non-entity parsers. Entity links reject them, but a join or add-community link picking up a tracking param is plausible enough that tightening it belongs in its own change.

Verified locally: complete Tauri library suite 2443 passed / 15 ignored / 0 failed (52 in the deep-link module, 49 before), cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets -- -D warnings clean, cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --all -- --check, git diff --check. Not run: the OS-level handoff itself — these are the parsers, exercised directly.

deep_link.rs carried three different policies for a duplicated query
parameter. parse_entity_deep_link refuses the URL, parse_websocket_relay_param
and optional_non_empty_param take the first value, and parse_message_deep_link
and parse_join_deep_link assigned in a loop, so they took the last.

The in-app parsers take the first: messageLink.ts reads searchParams.get, and
entityLink.ts refuses outright. So the same URL could route one way when
clicked inside the app and another when handed to the app by the OS, and a
join link could display one code while carrying another.

single_param is now the one reader: absent, empty or repeated all resolve to
None, and every query-param parser in the file goes through it. That matches
what our own builders emit, which is never a duplicate.

Signed-off-by: Taksh <takshkothari09@gmail.com>
parse_channel_deep_link, twenty lines up, parses the channel UUID and
requires the message id to be 64 hex characters, lowercasing it. The query
form of the same two identifiers accepted anything non-empty, so a link with
a malformed channel or event id produced a payload the frontend could never
route, and one whose id differed only in case looked like a different
message.

Both are now checked the same way. An absent or empty thread param still
reads as absent, as before; a present but malformed one is a rejection.

The four tests that used placeholder values ("abc", "xyz", "root1") now use a
real UUID and real event ids. They were asserting which params are required,
not that malformed values are accepted — the parser beside them has always
refused those exact strings.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Three message cases and two join cases, each the shape the old loop resolved
to its last value: a second channel, a second id, a second thread, a second
code, a second relay. The join pair is the one worth having — a link that
shows one code and carries another is the whole reason to prefer refusing
over picking.

Signed-off-by: Taksh <takshkothari09@gmail.com>
@Chessing234
Chessing234 requested a review from a team as a code owner August 16, 2026 20:18

@themiguelamador themiguelamador 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.

I found two remaining duplicate-parameter paths:

  • single_param returned the same None for “absent” and “repeated.” That works for required parameters, but optional policy_receipt and add-community name treated a duplicate as a valid absence and continued parsing.
  • Nostr-bind required fields still used the old first-value reader, while optional callback_url had the same absent/repeated ambiguity. This left security-sensitive bind links outside the PR's stated single policy.

I fixed these in commit 6c1438be1 on Complear:review/pr-6080-fix. The shared reader now distinguishes valid absence/empty from invalid repetition, all parser families consume that distinction, and Nostr-bind reports an explicit repeated <param> error.

Verification:

  • focused Tauri deep-link suite — 54 passed
  • cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets -- -D warnings
  • cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --all -- --check
  • desktop Tauri pre-commit format hook
  • git diff --check

…ms too

Review finding (P1, themiguelamador on block#6080): two families were still outside
the policy this PR claims to establish.

- `single_param` returned the same `None` for "absent" and "repeated". That is
  fine for a required param, where both fail the link, but optional
  `policy_receipt` and add-community `name` read a duplicate as a valid absence
  and carried on parsing — the link was accepted with the field silently
  dropped, which is exactly the ambiguity the policy removes.
- Nostr-bind's required fields still went through the old first-value reader,
  and its optional `callback_url` had the same absent/repeated ambiguity. That
  one is security-sensitive: the callback is checked against `origin` only when
  present, so a repeated `callback_url` reading as absent hands back a bind
  payload whose callback never met `validate_nostr_bind_callback_url`.

`read_single_param` now returns three outcomes — `Absent`, `Present`,
`Repeated` — and every reader is a projection of it: required params fail on
absent and repeated alike, optional params keep absence valid and reject
repetition, and nostr-bind reports `repeated <param>` by name rather than the
misleading `missing <param>`.

Signed-off-by: Taksh <takshkothari09@gmail.com>
@Chessing234

Copy link
Copy Markdown
Contributor Author

Both confirmed and fixed in 795799d. (Complear/buzz 404s, so written from your description.)

You've identified the actual shape of the mistake: single_param collapsed "absent" and "repeated" into one None, which is fine for a required parameter — both fail the link — and wrong for an optional one, where the caller reads it as "not supplied" and carries on. So policy_receipt and add-community name accepted the link with the field silently dropped, which is exactly the ambiguity the PR claims to remove.

The nostr-bind gap is the more serious half, and for the reason you give: callback_url is checked against origin only when present, so a repeated one reading as absent hands back a bind payload whose callback never met validate_nostr_bind_callback_url. Its required fields were also still on the old first-value .find().

read_single_param now returns Absent | Present | Repeated and every reader is a projection of it — required fails on absent and repeated alike, optional keeps absence valid and rejects repetition, and nostr-bind reports repeated <param> by name rather than the misleading missing <param>. Added regressions for repeated policy_receipt, repeated add-community name, five repeated bind required params, and repeated callback_url.

Verification: focused deep-link suite (55 passed), full Tauri lib suite (2,446 passed, 15 ignored), workspace/all-target clippy with -D warnings, cargo fmt --check, git diff --check.

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.

2 participants