Skip to content

Commit 9dc3078

Browse files
committed
Validate favicon hosts against the Public Suffix List (tldts)
Replace the ad-hoc .local/private-IP regex with a PSL check via tldts: only emit a favicon for a registrable domain under an ICANN suffix. This correctly excludes .local/.internal hosts, single-label names, IPs, and invalid TLDs, while handling multi-part suffixes (gov.in, gov.uk, …). The homepage client now trusts the server-computed api.json icon instead of re-deriving the URL.
1 parent 9d0c584 commit 9dc3078

6 files changed

Lines changed: 21 additions & 26 deletions

File tree

output/cli.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@
411411
"name": "gcloud",
412412
"description": "Manage Google Cloud resources, IAM, and deployments.",
413413
"url": "https://cloud.google.com/sdk/gcloud",
414-
"icon": "https://cloud.google.com/favicon.ico",
414+
"icon": "https://google.com/favicon.ico",
415415
"categories": [],
416416
"feeds": [
417417
"cli-seed"

output/index.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/openapi.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:7d0b60f9d8ddc2fe01f481554f540d2d37e3886d48f0c3c782d1a02cb0192dfa
3-
size 58957466
2+
oid sha256:53aaa337540f3bad7f4763585d648ddaa22b6579668f7b51821453f3f2080f50
3+
size 58957376

public/api.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/lib/favicon.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
// Private/non-routable host ranges. We must NOT emit favicon URLs for these:
2-
// a public page requesting a LAN address (`.local`, localhost, private IP)
3-
// triggers the browser's Local Network Access permission prompt.
4-
const PRIVATE_IP = /^(10|127)\.\d|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\./;
1+
import { parse } from "tldts";
52

6-
/** Favicon URL for a domain, or null when the host isn't publicly routable. */
3+
/**
4+
* Favicon URL for a domain, or null when it isn't a real public registrable
5+
* domain. Validated against the Public Suffix List (via tldts): we require a
6+
* registrable domain under an ICANN suffix. This excludes `.local`/`.internal`
7+
* hosts, single-label names, IP addresses, and invalid TLDs — requesting a
8+
* favicon from any of those is wrong, and LAN hosts trigger the browser's
9+
* Local Network Access permission prompt. Multi-part suffixes (gov.in, gov.uk,
10+
* com.au, …) resolve correctly because the PSL knows them.
11+
*/
712
export function faviconUrl(domain: string | null | undefined): string | null {
813
if (!domain) return null;
9-
const d = domain.toLowerCase();
10-
if (!d.includes(".")) return null; // single-label host (not a public FQDN)
11-
if (d === "localhost" || d.endsWith(".local") || d.endsWith(".internal") || d.endsWith(".localhost")) return null;
12-
if (PRIVATE_IP.test(d)) return null;
13-
return `https://${domain}/favicon.ico`;
14+
const info = parse(domain);
15+
if (info.isIp || !info.domain || !info.isIcann) return null;
16+
return `https://${info.domain}/favicon.ico`;
1417
}

src/pages/index.astro

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@ const counts = {
7272

7373
const esc = (s) =>
7474
String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
75-
// Favicon URL, or "" for non-public hosts (.local / localhost / private IP)
76-
// — requesting those from a public page triggers a Local Network Access prompt.
77-
const fav = (d) => {
78-
const h = String(d).toLowerCase();
79-
if (!h.includes(".") || h === "localhost" || h.endsWith(".local") || h.endsWith(".internal") ||
80-
h.endsWith(".localhost") || /^(10|127)\.\d|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\./.test(h)) return "";
81-
return `https://${d}/favicon.ico`;
82-
};
8375
const dpath = (d) => `/d/${encodeURIComponent(d)}/`;
8476

8577
const buildGroups = (recs) => {
@@ -100,9 +92,9 @@ const counts = {
10092

10193
const groupRowHtml = (g) => {
10294
const ph = esc((g.domain[0] || "?").toUpperCase());
103-
const src = g.icon || fav(g.domain);
104-
const icon = src
105-
? `<img class="c-fav" src="${esc(src)}" alt="" loading="lazy" width="18" height="18" onerror="this.outerHTML='<span class=&quot;ph&quot;>${ph}</span>'">`
95+
// g.icon is the PSL-validated favicon URL from api.json (null for non-public hosts).
96+
const icon = g.icon
97+
? `<img class="c-fav" src="${esc(g.icon)}" alt="" loading="lazy" width="18" height="18" onerror="this.outerHTML='<span class=&quot;ph&quot;>${ph}</span>'">`
10698
: `<span class="ph">${ph}</span>`;
10799
const chips = TORDER.filter((t) => g.types[t]).map((t) => {
108100
const c = g.types[t];

0 commit comments

Comments
 (0)