diff --git a/packages/app/vite-rss.ts b/packages/app/vite-rss.ts index 4936b977..1f6b2c94 100644 --- a/packages/app/vite-rss.ts +++ b/packages/app/vite-rss.ts @@ -1,6 +1,7 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' import Parser from 'rss-parser' +import { lookup } from 'dns/promises' import { validateDevAuth, setCorsHeaders } from './server/dev-auth' export interface RssArticle { @@ -57,30 +58,47 @@ async function tryParseFeed(parser: Parser, feedUrl: string): Promise { + try { + const result = await lookup(hostname) + return !isPrivateIp(result.address) + } catch { + return false // DNS failure = block + } +} + function discoverFeedUrl(siteUrl: string): string[] { try { if (isPrivateUrl(siteUrl)) return [] const u = new URL(siteUrl) + // Only allow http(s) schemes + if (u.protocol !== 'https:' && u.protocol !== 'http:') return [] const base = `${u.protocol}//${u.host}` return RSS_PATHS.map((path) => base + path) } catch { @@ -98,6 +116,12 @@ async function fetchRssFromUrls(urls: string[]): Promise { const articles: RssArticle[] = [] for (const url of urls.slice(0, 5)) { + // Post-DNS SSRF: verify resolved IP is not private + try { + const hostname = new URL(url).hostname + if (!(await validateResolvedIp(hostname))) continue + } catch { continue } + const candidates = discoverFeedUrl(url) for (const feedUrl of candidates) { const items = await tryParseFeed(parser, feedUrl)