fix(desktop): prevent SSRF in automatic link-title previews - #63171
fix(desktop): prevent SSRF in automatic link-title previews#63171zapabob wants to merge 3 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing a real exposed path. The SSRF premise is confirmed on current main: apps/desktop/electron/main.ts:3950 follows redirects with curl, and main.ts:4095-4099 falls back to BrowserWindow.loadURL(rawUrl).
Problems
- Blocking:
apps/desktop/electron/link-title-fetch.ts:126-132does not rejectfec0::/10. Consequently, a DNS answer such asfec0::1passesisPublicIpv6()and reaches the pinned curl request, so the proposed public-address boundary is incomplete.
Suggested changes
- Reject
fec0::/10and add a resolver regression test forfec0::1. - Preserve current main's Vitest electron-test wiring (
apps/desktop/vitest.config.ts:15-20,apps/desktop/package.json:47) when salvaging this stale-base test change.
Automated hermes-sweeper review.
|
I reproduced a user-visible consequence of the hidden When a restored Desktop transcript contains a This is separate from the SSRF premise, but it supports this PR's approach of removing the renderer fallback: DNS/network pinning alone would not prevent a public page from invoking credential UI. For immediate local mitigation I blocked Suggested regression coverage: assert that Google Docs URLs never reach |
6a3941b to
65bfbed
Compare
(cherry picked from commit f599ad3ecf86e8eeefe826d8bc979682dd6ce0c0)
(cherry picked from commit b5318b886a3b5b677361920b64499a165770d405)
(cherry picked from commit 65bfbeddaf900b922e269480e5ea045a07a9c44c)
65bfbed to
4fc68b0
Compare
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Two PRs address the automatic Desktop link-title preview attack surface. #63171 is a compact curl-only hardening that validates and DNS-pins every hop, while #65613 is a broader implementation that centralizes URL admission and routes curl through a DNS-pinned SOCKS gateway; both remove the hidden renderer fallback.
Related pull requests
- #63171
related— (+506/-197) — overlapping fix with a concrete salvage path: validates public HTTP(S) targets, pins approved DNS answers with curl--resolve, revalidates redirects, bounds resource use, and removes arbitrary-pageBrowserWindownavigation. The contributor keep_open review correctly identifies this as an exposed path, but its claimedfec0::/10gap is contradicted by the diff:/^fe[c-f]/rejects that range, and the included resolver test already expectsfec0::1to be rejected. - #65613 [closed]
duplicate— (+2164/-415) — closed broader reference implementation: adds shared canonical URL admission, comprehensive special-address coverage, pinned DNS caching, a loopback SOCKS transport for curl, explicit redirect admission, deadline and shutdown handling, and removes the renderer fallback. The contributor keep_open review flagged renderer navigation and an unregistered E2E test on an earlier revision; the final diff addresses both by deleting the renderer path and its E2E file, but the PR remains relevant only as a reference because the author requested closure.
Duplicates
#63171 and #65613 substantially duplicate the same SSRF and hidden-renderer fix; #65613 additionally implements a larger shared URL-admission and pinned SOCKS transport architecture.
Suggested consolidation
Keep #63171 open with a salvage path: preserve its smaller curl-only change, explicit per-hop DNS pinning, bounded queue, renderer-fallback removal, and focused regression coverage, while checking its test wiring against the current Desktop Vitest configuration during rebase. Use closed #65613 as the reference for broader canonical URL and special-address test cases if they can be adopted without importing its full transport architecture; leave #65613 closed as the duplicate/reference rather than reopening it over the earlier keep_open review and the author's explicit closure request.
Complex graph
flowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
subgraph Dup63171 ["PRs duplicating each other"]
P63171["PR #63171 (open)"]
P65613["PR #65613 (closed)"]
end
class P63171 open
class P65613 closed
class P63171 target
click P63171 "https://github.com/NousResearch/hermes-agent/pull/63171"
click P65613 "https://github.com/NousResearch/hermes-agent/pull/65613"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).
Cross-PR triage: Reviewed 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 111 kB of PR diffs, 3 kB of issue/PR text, 7 kB of discussion (10 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
|
too large to review safely This PR changes 703 production lines before tests and docs. Please split it or add a focused justification if it should stay together. Signed: GPT-5.6-luna-high in Codex |
fix(desktop): prevent SSRF in automatic link-title previews
|
…ite-local IPv6 Adopts the security shape of upstream NousResearch#63171 (fetchPinnedLinkTitle, and the site-local classification from 65bfbeddaf) rather than parallel-implementing it — review feedback on this PR was exactly that these two primitives belong to that foundation, not a third variant. The hop guard now RETURNS the addresses it vetted, and the transport is required to pin the connection to one of them (curl --resolve host:port:addr, one per vetted address — curl fails over among pinned addresses without a fresh lookup). Before, the guard resolved the name, called it public, and then let curl resolve it AGAIN: an attacker controlling DNS could swap the answer to a private address between verdict and request. The pin closes that window for the tier-1 curl walk; tier-2's window guard is unchanged (its residual is documented in link-title-window.ts). isPrivateAddress also refuses fec0::/10 (site-local): deprecated, but still routable in some estates, and upstream already rejects it for link titles. Regression proof is the pinning contract itself: each hop's request carries the addresses vetted for THAT hop, and a site-local-only name is refused before any request.
Summary
--resolve, preventing DNS rebinding.Security impact
Untrusted links may enter the desktop client through agent/tool output. Previously automatic preview fetching could reach a local or otherwise non-public address, including through redirects or DNS rebinding. This change makes the title-preview path fail closed for those targets.
Validation
npm exec -- tsx --test electron/link-title-fetch.test.ts— 5 passednpm run typecheck— passednpm run build— passedgit diff --check upstream/main...HEAD— passednpm run linthas pre-existing unrelated import-order errors in the upstream renderer. The changed-file lint has no errors. The existing platform script invokes TypeScript test sources with plainnode --teston this host, so focused tests use the repository's installedtsxrunner.No fork-only plugins or
_docsfiles are included.