fix: stop favicon requests for private link hosts on web and mobile - #5838
fix: stop favicon requests for private link hosts on web and mobile#5838fe-franco wants to merge 3 commits into
Conversation
MarkdownExternalLink built a Google favicon URL from any external link host. resolveMarkdownLinkPresentation marks every http and https link as external, so a link to a dev server or a Tailscale host sent that private host name to Google. The request also always failed, because Google cannot resolve a private host. Add isPrivateLinkHost and start the component in the failed state for such a host, so it draws the existing fallback glyph and sends no request. Resolves pingdotgg#5835 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cfe25ac. Configure here.
ApprovabilityVerdict: Approved 575e128 This PR adds a privacy fix that prevents favicon requests for private/internal hosts (localhost, LAN IPs, Tailscale addresses, .local domains). The change is defensive - it triggers existing fallback behavior earlier while preventing hostname leakage to external services. Well-tested utility with clear scope. You can customize Macroscope's approvability policy. Learn more. |
The same defect exists in two more places than the mobile thread feed: - apps/web ChatMarkdown.tsx MarkdownLinkFavicon, the web chat renderer - apps/web favicon.ts faviconUrlForOrigin, the preview tab strip The preview tab strip matters most. A preview URL usually points at a dev server or a tailnet host, so it leaked a private host on the normal path. Move isPrivateHost into packages/shared, which every app already depends on, and guard all three call sites. The shared module keeps no runtime dependency, so web and React Native can both import it. Net.ts was not an option because it imports node:net. faviconUrlForOrigin checks url.hostname, not url.host, because host keeps the port and localhost:5173 would slip past the check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isPrivateHost let three private forms through: - IPv4-mapped IPv6, both ::ffff:192.168.1.10 and ::ffff:c0a8:010a - a trailing DNS root label, such as printer.local. - names under .localhost, which RFC 6761 reserves for loopback Add ipv4FromMappedIpv6 and delegate the embedded address to isPrivateIpv4. Strip one trailing dot before the suffix checks. Add .localhost to the suffix list. Each gap gets a test, including the public counterexamples ::ffff:8.8.8.8 and github.com. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed all three findings from bugbot and macroscope, checked each against the source and fixed IPv4-mapped IPv6 (macroscope). Confirmed. Trailing DNS root label (macroscope). Confirmed. Names under .localhost (cursor). Confirmed, and RFC 6761 reserves the whole Both macroscope comments carried a "no longer relevant" note, because the file Each gap has a test, with a public counterexample beside it: |

Resolves #5843
What Changed
Three call sites built a Google favicon URL from a link host. All three now skip
the request when the host is private, and each draws the fallback it already
has.
isPrivateHostlives inpackages/shared/src/privateHost.ts, because every appalready depends on
@t3tools/shared. The module keeps no runtime dependency, soweb and React Native can both import it.
Net.tswas not an option, because itimports
node:net.The predicate covers
localhostand any bare host name. It covers the.local,.internal,.home.arpaand.ts.netsuffixes. For IPv4 it covers RFC 1918,loopback, link local, and the Tailscale
100.64.0.0/10range. For IPv6 it covers::1,fc00::/7andfe80::/10.Why
resolveMarkdownLinkPresentationmarks everyhttp:andhttps:link asexternal, so the host reaching the favicon provider can be private. A link to
http://192.168.1.10:8080sent192.168.1.10to Google. A link tohttps://box.tailnet.ts.netsent the tailnet name.The preview tab strip matters most. Its own doc comment describes it as a preview
helper, and a preview URL usually points at a dev server or a tailnet host. So it
leaked a private host on the normal path, not the rare one. Issue #5818 shows
users pairing over
100.64.0.0/10.Every one of these requests also failed already, because Google cannot resolve a
private host. So this removes a doomed request as well as the disclosure.
One detail worth review:
faviconUrlForOriginchecksurl.hostname, noturl.host.hostkeeps the port, solocalhost:5173would slip past the check.The
domainparameter still usesurl.host, which keeps the existing behaviorfor a public origin.
UI Changes
None. Each private host already failed its favicon load and fell back to the
glyph, the globe, or null. The rendered result is the same. Only the outbound
request disappears.
Checklist
Verification
packages/shared/src/privateHost.test.tscover each range. Theyassert that
100.63.255.255,100.128.0.1,172.32.0.1and8.8.8.8staypublic.
apps/web/src/lib/favicon.test.tscover the new guard, a publicorigin, a port, and the existing null paths.
vp run --filter @t3tools/shared testpasses, 341 tests in 42 files.vp run --filter @t3tools/web testpasses, 2000 tests in 221 files.vp run --filter @t3tools/mobile testpasses, 620 tests in 100 files.vp run -r --concurrency-limit 2 typecheckexits 0 with no errors.vp lint --report-unused-disable-directivesreports one pre-existing warningin
apps/web/src/components/settings/ThemeEditorPanel.tsx, untouched here.vp fmt --checkpasses.1a003e383.I did not check whether
apps/desktopinherits the web path. It loadst3code-dev://app/through a custom protocol, and it declares no dependency on@t3tools/web, so I could not confirm either way.Written by Claude Opus 5 in Claude Code.
Note
Low Risk
Client-side guard only; no auth or data-path changes, and behavior matches prior failed-load fallbacks.
Overview
Stops leaking internal hostnames to Google’s favicon service when chat links or preview tabs point at dev servers, LAN IPs, or tailnet names.
Adds shared
isPrivateHost(@t3tools/shared/privateHost) and wires it intofaviconUrlForOrigin(returnsnullfor privatehostname), webMarkdownLinkFavicon, and mobileMarkdownExternalLinkso those paths skip the provider URL and keep the existing globe/glyph fallbacks. UI stays the same; doomed outbound requests go away.Reviewed by Cursor Bugbot for commit 575e128. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Stop favicon requests for private link hosts on web and mobile
isPrivateHost(host)in packages/shared/src/privateHost.ts, detecting private IPv4/IPv6 ranges, Tailscale (100.64.0.0/10), and private TLD suffixes (.local,.internal,.ts.net, etc.).faviconUrlForOriginnow returnsnullfor private hosts instead of building a provider URL.MarkdownLinkFaviconrenders the globe icon fallback and mobile'sMarkdownExternalLinkskips the favicon fetch entirely for private hosts.Macroscope summarized 575e128.