Skip to main content
Glama
urlValidator.js4.73 kB
import Joi from 'joi'; import { URL } from 'url'; // Configure allowed domains for image downloads const ALLOWED_DOMAINS = [ // Common CDNs and image hosting services 'imgur.com', 'i.imgur.com', 'github.com', 'githubusercontent.com', 'unsplash.com', 'images.unsplash.com', 'cloudinary.com', 'res.cloudinary.com', 'amazonaws.com', 's3.amazonaws.com', 'googleusercontent.com', 'gravatar.com', 'secure.gravatar.com', 'wp.com', 'wordpress.com', 'flickr.com', 'staticflickr.com', 'dropbox.com', 'dl.dropboxusercontent.com', 'pexels.com', 'images.pexels.com', 'pixabay.com', 'cdn.pixabay.com' ]; // Private/internal IP ranges to block const BLOCKED_IP_PATTERNS = [ /^127\./, // 127.0.0.0/8 - localhost /^10\./, // 10.0.0.0/8 - private /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 - private /^192\.168\./, // 192.168.0.0/16 - private /^169\.254\./, // 169.254.0.0/16 - link local /^0\./, // 0.0.0.0/8 /^224\./, // 224.0.0.0/4 - multicast /^240\./, // 240.0.0.0/4 - reserved /^255\.255\.255\.255$/, // broadcast /^::1$/, // IPv6 localhost /^::/, // IPv6 unspecified /^fc00:/, // IPv6 unique local /^fe80:/, // IPv6 link local /^ff00:/ // IPv6 multicast ]; /** * Validates if a hostname/IP is safe for external requests * @param {string} hostname - The hostname or IP to validate * @returns {boolean} True if safe, false if blocked */ const isSafeHost = (hostname) => { // Check if hostname is an IP address const ipv4Pattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; const ipv6Pattern = /^[0-9a-f:]+$/i; if (ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname)) { // Check against blocked IP patterns return !BLOCKED_IP_PATTERNS.some(pattern => pattern.test(hostname)); } // For domain names, check against allowlist const normalizedHost = hostname.toLowerCase(); return ALLOWED_DOMAINS.some(allowed => normalizedHost === allowed || normalizedHost.endsWith('.' + allowed) ); }; /** * Validates and sanitizes a URL for safe external requests * @param {string} url - The URL to validate * @returns {object} Validation result with isValid boolean and sanitizedUrl */ const validateImageUrl = (url) => { try { // Basic URL validation with Joi const urlSchema = Joi.string().uri({ scheme: ['http', 'https'], allowRelative: false }).required(); const validation = urlSchema.validate(url); if (validation.error) { return { isValid: false, error: `Invalid URL format: ${validation.error.details[0].message}` }; } // Parse URL for additional security checks const parsedUrl = new URL(url); // Only allow HTTP/HTTPS if (!['http:', 'https:'].includes(parsedUrl.protocol)) { return { isValid: false, error: 'Only HTTP and HTTPS protocols are allowed' }; } // Check if host is safe (not internal/private) if (!isSafeHost(parsedUrl.hostname)) { return { isValid: false, error: `Requests to ${parsedUrl.hostname} are not allowed for security reasons` }; } // Prevent requests to non-standard ports (common in SSRF attacks) const port = parsedUrl.port; if (port && !['80', '443', '8080', '8443'].includes(port)) { return { isValid: false, error: `Requests to non-standard port ${port} are not allowed` }; } // Additional checks for suspicious patterns if (parsedUrl.hostname.includes('localhost') || parsedUrl.hostname === '0.0.0.0' || parsedUrl.hostname.startsWith('192.168.') || parsedUrl.hostname.startsWith('10.') || parsedUrl.hostname.includes('.local')) { return { isValid: false, error: 'Requests to local/private addresses are not allowed' }; } return { isValid: true, sanitizedUrl: parsedUrl.href }; } catch (error) { return { isValid: false, error: `URL parsing failed: ${error.message}` }; } }; /** * Configures axios with security settings for external requests * @param {string} url - The validated URL to request * @returns {object} Axios configuration with security settings */ const createSecureAxiosConfig = (url) => { return { url, responseType: 'stream', timeout: 10000, // 10 second timeout maxRedirects: 3, // Limit redirects maxContentLength: 50 * 1024 * 1024, // 50MB max response validateStatus: (status) => status >= 200 && status < 300, // Only accept 2xx headers: { 'User-Agent': 'Ghost-MCP-Server/1.0' } }; }; export { validateImageUrl, createSecureAxiosConfig, ALLOWED_DOMAINS };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jgardner04/Ghost-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server