/** * @file avatar-url.js * @description Checks an avatar LINK the account typed (Settings → Account, * `POST /api/me/avatar/url`). The link is stored as it is and the avatar * routes redirect the viewer's browser to it — the server never downloads it, * so a picture behind the viewer's own login or on a company-internal host * (e.g. a Mattermost `/api/v4/users//image`) still shows. Held to: * `https://`, no userinfo, at most `MAX_LINK_LENGTH` characters, and no * loopback/private/link-local/reserved IP literal as the host (a link may not * aim every viewer's browser at their own machine or LAN). * @author Nguyễn Ngọc Trí Vĩ */ const net = require("node:net"); const MAX_LINK_LENGTH = 2048; const fail = (code, message) => Object.assign(new Error(message), { code }); const blocked = new net.BlockList(); for (const [addr, bits] of [ ["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8], ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.0.2.0", 24], ["192.88.99.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15], ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4], ]) { blocked.addSubnet(addr, bits, "ipv4"); } for (const [addr, bits] of [ ["::", 128], ["::1", 128], ["64:ff9b::", 96], ["64:ff9b:1::", 48], ["100::", 64], ["2001::", 23], ["2001:db8::", 32], ["2002::", 16], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8], ]) { blocked.addSubnet(addr, bits, "ipv6"); } /** True only for a globally routable unicast address. */ function isPublicAddress(address) { const family = net.isIP(address); if (family === 4) return !blocked.check(address, "ipv4"); if (family !== 6) return false; const lower = address.toLowerCase(); // An IPv4-mapped/compatible address is judged as the IPv4 address it carries. const mapped = /^::(?:ffff:)?(\d+\.\d+\.\d+\.\d+)$/.exec(lower); if (mapped) return isPublicAddress(mapped[1]); if (/^::(?:ffff:)?[0-9a-f]{1,4}:[0-9a-f]{1,4}$/.test(lower)) return false; // Only global unicast (2000::/3) is public. if (!/^[23][0-9a-f]{0,3}:/.test(lower)) return false; return !blocked.check(address, "ipv6"); } /** The link, normalised, or EBADURL / EPRIVATEURL: https only, no userinfo, a real host. */ function parseAvatarUrl(text) { const raw = String(text ?? "").trim(); if (raw.length > MAX_LINK_LENGTH) throw fail("EBADURL", "the link is too long"); let url; try { url = new URL(raw); } catch { throw fail("EBADURL", "not a valid URL"); } if (url.protocol !== "https:") throw fail("EBADURL", "only an https:// link"); if (url.username || url.password) throw fail("EBADURL", "a link with a login is refused"); if (!url.hostname) throw fail("EBADURL", "not a valid URL"); const host = url.hostname.toLowerCase(); const literal = host.replace(/^\[|\]$/g, ""); if ( host === "localhost" || host.endsWith(".localhost") || (net.isIP(literal) && !isPublicAddress(literal)) ) { throw fail("EPRIVATEURL", "that address is not on the public internet"); } return url.href; } module.exports = { MAX_LINK_LENGTH, isPublicAddress, parseAvatarUrl };