Skip to content

fix: stop favicon requests for private link hosts on web and mobile - #5838

Open
fe-franco wants to merge 3 commits into
pingdotgg:mainfrom
fe-franco:fix/skip-favicon-for-private-hosts
Open

fix: stop favicon requests for private link hosts on web and mobile#5838
fe-franco wants to merge 3 commits into
pingdotgg:mainfrom
fe-franco:fix/skip-favicon-for-private-hosts

Conversation

@fe-franco

@fe-franco fe-franco commented Aug 9, 2026

Copy link
Copy Markdown

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.

apps/mobile/src/features/threads/ThreadFeed.tsx:284   MarkdownExternalLink   glyph
apps/web/src/components/ChatMarkdown.tsx:858          MarkdownLinkFavicon    GlobeIcon
apps/web/src/lib/favicon.ts:10                        faviconUrlForOrigin    null

isPrivateHost lives in packages/shared/src/privateHost.ts, because every app
already depends on @t3tools/shared. The 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.

The predicate covers localhost and any bare host name. It covers the .local,
.internal, .home.arpa and .ts.net suffixes. For IPv4 it covers RFC 1918,
loopback, link local, and the Tailscale 100.64.0.0/10 range. For IPv6 it covers
::1, fc00::/7 and fe80::/10.

Why

resolveMarkdownLinkPresentation marks every http: and https: link as
external, so the host reaching the favicon provider can be private. A link to
http://192.168.1.10:8080 sent 192.168.1.10 to Google. A link to
https://box.tailnet.ts.net sent 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: faviconUrlForOrigin checks url.hostname, not
url.host. host keeps the port, so localhost:5173 would slip past the check.
The domain parameter still uses url.host, which keeps the existing behavior
for 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

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Verification

  • 7 tests in packages/shared/src/privateHost.test.ts cover each range. They
    assert that 100.63.255.255, 100.128.0.1, 172.32.0.1 and 8.8.8.8 stay
    public.
  • 5 tests in apps/web/src/lib/favicon.test.ts cover the new guard, a public
    origin, a port, and the existing null paths.
  • vp run --filter @t3tools/shared test passes, 341 tests in 42 files.
  • vp run --filter @t3tools/web test passes, 2000 tests in 221 files.
  • vp run --filter @t3tools/mobile test passes, 620 tests in 100 files.
  • vp run -r --concurrency-limit 2 typecheck exits 0 with no errors.
  • vp lint --report-unused-disable-directives reports one pre-existing warning
    in apps/web/src/components/settings/ThemeEditorPanel.tsx, untouched here.
  • vp fmt --check passes.
  • Rebased onto 1a003e383.

I did not check whether apps/desktop inherits the web path. It loads
t3code-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 into faviconUrlForOrigin (returns null for private hostname), web MarkdownLinkFavicon, and mobile MarkdownExternalLink so 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

  • Adds 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.).
  • In apps/web/src/lib/favicon.ts, faviconUrlForOrigin now returns null for private hosts instead of building a provider URL.
  • Web's MarkdownLinkFavicon renders the globe icon fallback and mobile's MarkdownExternalLink skips the favicon fetch entirely for private hosts.
  • Behavioral Change: private-host links no longer trigger outbound favicon requests, which previously leaked internal hostnames to the favicon provider.

Macroscope summarized 575e128.

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>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01af0589-7a37-4bc1-89ac-bafb03f79b6d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 9, 2026
Comment thread packages/shared/src/privateHost.ts
Comment thread packages/shared/src/privateHost.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/shared/src/privateHost.ts
@macroscopeapp

macroscopeapp Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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>
@fe-franco fe-franco changed the title fix(mobile): stop favicon requests for private link hosts fix: stop favicon requests for private link hosts on web and mobile Aug 9, 2026
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>
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 9, 2026
@fe-franco

fe-franco commented Aug 9, 2026

Copy link
Copy Markdown
Author

Fixed all three findings from bugbot and macroscope, checked each against the source and fixed
them in 575e128.

IPv4-mapped IPv6 (macroscope). Confirmed. ::ffff:192.168.1.10 split on .
into four parts, but the first part ::ffff:192 failed the digit test, so
isPrivateIpv4 returned false. isPrivateIpv6 never matched either, because the
address starts with a colon. Added ipv4FromMappedIpv6, which handles the dotted
form and the hex form ::ffff:c0a8:010a, then delegates to isPrivateIpv4.

Trailing DNS root label (macroscope). Confirmed. printer.local. failed
endsWith(".local"). isPrivateHost now strips one trailing dot before every
check.

Names under .localhost (cursor). Confirmed, and RFC 6761 reserves the whole
subtree for loopback. Added .localhost to the suffix list.

Both macroscope comments carried a "no longer relevant" note, because the file
moved from apps/mobile/src/lib/privateLinkHost.ts to
packages/shared/src/privateHost.ts in f763b8a. The logic moved unchanged, so
both bugs were still live. I treated them as current.

Each gap has a test, with a public counterexample beside it: ::ffff:8.8.8.8,
::ffff:808:808 and github.com. all stay public. Shared tests go from 341 to
344, and web, mobile and typecheck stay green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: favicon requests send private link hosts to Google

1 participant