Skip to content

[Bug]: Link favicons send internal company hostnames to Google, and preview tabs also send the port #16894

Description

@Godatplay

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

packages/contracts or packages/shared

Steps to reproduce

Follow-up to #5843. That fix stops private hosts (localhost, RFC 1918, Tailscale, .local, .internal, …) from going to Google. Two leaks remain for publicly-named hosts, and they matter for confidential/company work:

A. Chat links send confidential hostnames to Google

  1. Run the desktop app with a network monitor (Little Snitch, LuLu, or DevTools → Network).
  2. In a thread, have a message (user or agent) contain a link to an internal tool on a company domain, e.g. https://grafana.internal.acme-corp.com/d/abc/latency or https://jira.corp.acme-corp.com/browse/SEC-112.
  3. MarkdownLinkFavicon renders and loads https://www.google.com/s2/favicons?domain=grafana.internal.acme-corp.com&sz=32.

B. Preview tabs also send the port

  1. Open a preview/browser tab to https://staging-api.acme-corp.com:8443/.
  2. While no captured favicon is available for that origin (before the page's favicon is captured, or the page has none, or in the web app where there is no desktop capture), PreviewFavicon loads https://www.google.com/s2/favicons?domain=staging-api.acme-corp.com%3A8443&sz=32.

Expected behavior

The app does not send the hostnames of links in my threads, or the host:port of pages I preview, to a third party. Or at least it lets me turn that off. Agents write links to internal dashboards, tickets, staging environments and self-hosted Git hosts all the time, so this happens even if the user never pastes a link.

If Google lookups are kept, the request should never include the port.

Actual behavior

faviconUrlForOrigin (packages/shared/src/favicon.ts:86-97, main @ 4ce6a51) gates on isPublicFaviconHost(url.hostname) but builds the request from url.host:

if (!isPublicFaviconHost(url.hostname)) return null;
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(url.host)}&sz=${size}`;
  1. Hostname leak. isPublicFaviconHost (packages/shared/src/hostClassification.ts:123) only rejects names that can't resolve publicly. Any name under a registered domain passes, including ones that are obviously internal by convention. I ran the shipped filter from the 0.0.46 desktop bundle against sample hosts:

    Host Sent to Google?
    grafana.internal.acme-corp.com yes (.internal only matches as a suffix)
    jira.corp.acme-corp.com, staging-api.acme-corp.com, gitlab.acme-corp.com yes
    acme-corp.atlassian.net (reveals vendor/customer relationships) yes
    grafana.corp, build.lan (common private naming not in the suffix list) yes
    public IPs such as 52.1.2.3 yes
    localhost, 10.x, 192.168.x, 100.64/10, *.ts.net, *.local, single-label names, .internal/.test suffixes no
  2. Port leak. The filter checks url.hostname, but the query uses url.host, which includes the port. Chat links aren't affected, because web (resolveExternalWebLinkHost → .hostname) and mobile (markdownLinks.ts:275 → parsed.hostname) pass a hostname with no port. The preview tab is affected: RightPanelTabs.tsx:619-627 passes the full navigated URL to faviconUrlForOrigin(url, 32). A non-default port on a staging, admin or debug service tells a third party how the infrastructure is laid out, and it's never needed for a favicon lookup.

Call sites on main @ 4ce6a51:

apps/web/src/components/ChatMarkdown.tsx:1502            MarkdownLinkFavicon   (hostname only)
apps/mobile/src/features/threads/ThreadFeed.tsx:755      markdown link icon    (hostname only)
apps/web/src/components/RightPanelTabs.tsx:620           PreviewFavicon        (full URL → host:port)

What Google gets each time: the hostname (and the port in case B), the user's IP, and a timestamp. No path or query, no thread content. For people doing confidential work, though, the list of internal hostnames they look at, linked to their IP, is sensitive in itself.

Impact

Major degradation or frequent failure

Version or commit

Desktop 0.0.46-preview.20261006.1 (Alpha); source references at main @ 4ce6a51

Environment

macOS 27.0.1, T3 Code (Alpha) desktop 0.0.46-preview.20261006.1

Logs or stack traces

# Requests produced by the code above (derived from source plus the shipped bundle, not a live capture):
GET https://www.google.com/s2/favicons?domain=grafana.internal.acme-corp.com&sz=32
GET https://www.google.com/s2/favicons?domain=staging-api.acme-corp.com%3A8443&sz=32

How this was established: a user noticed recurring google.com traffic from the desktop app in their firewall. I traced it to faviconUrlForOrigin in the shipped app.asar, ran the bundle's host filter against the hosts in the table above, and confirmed the call sites and the hostname/host mismatch on current main. Not checked: I did not capture live traffic for case B, so I haven't measured how often the Google fallback fires in practice compared with the captured desktop favicon.

Workaround

Block www.google.com for T3 Code in a firewall (Little Snitch/LuLu). Link and preview icons fall back to the globe glyph.

Possible fixes, for whoever picks this up:

  • Use encodeURIComponent(url.hostname) instead of url.host in faviconUrlForOrigin. This one-line change closes the port leak.
  • Prefer the site's own /favicon.ico (as faviconUrlForPage already does for tool activity) over a third-party lookup, or add a setting to disable third-party favicon lookups.

Activity

  1. juliusmarminge commented on Oct 7, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed write-up. I checked it against main @ 4ce6a51.

    Confirmed in code

    • Port leak. In packages/shared/src/favicon.ts:92-93, faviconUrlForOrigin gates on isPublicFaviconHost(url.hostname) but builds the Google URL from encodeURIComponent(url.host), so a non-default port goes out too. faviconUrlForOrigin("https://staging-api.acme-corp.com:8443/") returns https://www.google.com/s2/favicons?domain=staging-api.acme-corp.com%3A8443&sz=32. packages/shared/src/favicon.test.ts:30-34 currently asserts this ("keeps the public origin, port and requested size"), so that test needs to change with the fix.
    • Preview tabs hit this path. PreviewFavicon (apps/web/src/components/RightPanelTabs.tsx:619-627) passes the full navigated URL to faviconUrlForOrigin and hands [capturedUrl, publicProviderUrl] to FaviconImage. FaviconImage (apps/web/src/components/preview/PreviewFaviconIcon.tsx:9-43) loads the first source that hasn't failed. So when there's no captured favicon (capturedUrl is null unless the desktop capture matches the current origin, RightPanelTabs.tsx:653-660), or the captured image fails, the Google URL with host:port is requested.
    • Chat links send the hostname but not the port. Web MarkdownLinkFavicon (apps/web/src/components/ChatMarkdown.tsx:1499-1502) gets its host from resolveExternalWebLinkHost, which returns .hostname (apps/web/src/components/chat/externalLinkContextMenu.ts:91-92). Mobile MarkdownExternalLink (apps/mobile/src/features/threads/ThreadFeed.tsx:755) gets parsed.hostname (apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts:275).
    • What the filter lets through. isPublicFaviconHost (packages/shared/src/hostClassification.ts:123-162) rejects loopback, RFC 1918, CGNAT/Tailscale, .ts.net, .local, .localhost, home.arpa, single-label names, special-purpose IPs, and the .alt/.example/.internal/.invalid/.onion/.test suffixes. Every other dotted name returns true. I ran the function on main, and these all came back public: grafana.internal.acme-corp.com, jira.corp.acme-corp.com, staging-api.acme-corp.com, acme-corp.atlassian.net, grafana.corp, build.lan and 52.1.2.3. Private and Tailscale hosts are correctly excluded, so [Bug]: favicon requests send private link hosts to Google #5843's fix still holds for those.
    • faviconUrlForPage (favicon.ts:8-17) only builds <page origin>/favicon.ico, a first-party request with no third party involved. It's used only by toolActivityFaviconUrl (favicon.ts:61-83), not by chat links or preview tabs.

    Root cause

    faviconUrlForOrigin classifies on hostname but sends host. Separately, the public/private filter can only reject names that are private by syntax. It can't tell that *.corp.acme-corp.com is internal, so any publicly-shaped name a user or agent links to goes to Google.

    Fix direction

    • Use encodeURIComponent(url.hostname) in faviconUrlForOrigin and update the favicon.test.ts:30-34 expectation. This closes the port leak for preview tabs.
    • The broader question is whether chat links and preview tabs should use a third-party favicon lookup at all, or have a toggle for it, versus the site's own /favicon.ico like faviconUrlForPage. That's a product decision and isn't part of this bug fix.

    Workaround

    Block www.google.com for T3 Code in a per-app firewall (Little Snitch/LuLu). The favicon <img>/Image error handlers then fall back to a placeholder glyph: a globe on web and desktop (ChatMarkdown.tsx:1510-1520, FaviconImage), and ◉ on mobile (ThreadFeed.tsx:772-786).

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions