security: SSRF protection on user-supplied RSS URLs

Validate every RSS URL submitted via /api/podcasts/subscribe before
fetching. Rejects non-http(s), localhost, private IPv4/IPv6 ranges,
link-local, and cloud metadata endpoints (169.254.169.254). Resolves
hostnames via DNS and re-checks every returned address.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Haiku 4.5 committed 2026-04-18 07:25:33 +02:00
1 parent 9bf95b4ac9
commit 5497c89a99
2 files changed
+88

No files matched your search

@@ -5,6 +5,7 @@ import { UserSubscription } from '../models/UserSubscription';
import { Episode } from '../models/Episode';
import { podcastIndexService } from '../services/podcastIndexService';
import { rssParserService } from '../services/rssParserService';
import { validateExternalUrl } from '../utils/urlValidator';
import { z } from 'zod';
const subscribeSchema = z.object({
@@ -52,6 +53,13 @@ export const subscribe = async (req: Request, res: Response) => {
const { rssUrl } = subscribeSchema.parse(req.body);
// SSRF protection — block internal/private hosts
try {
await validateExternalUrl(rssUrl);
} catch (err: any) {
return res.status(400).json({ message: `URL refusée : ${err.message}` });
}
// Check if podcast exists
let podcast = await Podcast.findOne({ rssUrl });