UNPKG

@nanggo/social-preview

Version:

Generate beautiful social media preview images from any URL

98 lines (97 loc) 4.36 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateUrlInput = validateUrlInput; exports.validateImageUrl = validateImageUrl; exports.sanitizeUrl = sanitizeUrl; const types_1 = require("../../types"); const security_1 = require("../../constants/security"); const text_1 = require("./text"); /** * Comprehensive URL validation with security checks. */ function validateUrlInput(url) { if (!url || typeof url !== 'string') { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, 'URL must be a non-empty string'); } const sanitizedUrl = (0, text_1.sanitizeControlChars)(url.trim()); // Length check if (sanitizedUrl.length > security_1.MAX_URL_LENGTH) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, `URL exceeds maximum length of ${security_1.MAX_URL_LENGTH} characters`); } // Security patterns check if (!isSafeUrlInput(sanitizedUrl)) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, 'URL contains potentially dangerous characters or patterns'); } try { const urlObj = new URL(sanitizedUrl); // Protocol validation - URL.protocol is always lowercase, so direct comparison is safe const protocol = urlObj.protocol.toLowerCase(); if (!security_1.ALLOWED_PROTOCOLS.includes(protocol)) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, `Invalid protocol: ${protocol}. Only ${security_1.ALLOWED_PROTOCOLS.join(' and ')} are supported.`); } // Hostname validation - ensure hostname exists and is not empty if (!urlObj.hostname || urlObj.hostname.trim().length === 0) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, 'URL must have a valid hostname'); } // Additional security: reject URLs with unusual characters in hostname const hostnamePattern = /^[a-zA-Z0-9.-]+$/; if (!hostnamePattern.test(urlObj.hostname)) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, 'URL hostname contains invalid characters'); } return urlObj.toString(); } catch (error) { if (error instanceof types_1.PreviewGeneratorError) { throw error; } throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, `Invalid URL format: ${url}`); } } /** * Validate image URL with additional security checks. */ function validateImageUrl(imageUrl) { // First validate as regular URL const validatedUrl = validateUrlInput(imageUrl); // Additional checks specific to image URLs const urlObj = new URL(validatedUrl); // Check for suspicious query parameters for (const param of security_1.SUSPICIOUS_URL_PARAMS) { if (urlObj.searchParams.has(param)) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.VALIDATION_ERROR, `Image URL contains suspicious parameter: ${param}`); } } return validatedUrl; } /** * Validate and sanitize URL. */ function sanitizeUrl(url) { const validated = validateImageUrl(url); return validated; } function isSafeUrlInput(url) { // Check for blocked protocols - must check URL start, not anywhere in the string // to avoid false positives like "https://example.com/page?info=some_data:value" const lowerUrl = url.trim().toLowerCase(); for (const protocol of security_1.BLOCKED_PROTOCOLS) { if (lowerUrl.startsWith(protocol)) { return false; } } // Check for dangerous HTML/Script patterns for (const pattern of security_1.DANGEROUS_HTML_PATTERNS) { // Create new RegExp to avoid global flag state issues const testPattern = new RegExp(pattern.source, pattern.flags); if (testPattern.test(url)) { return false; } } // Check for control characters const asciiPattern = new RegExp(security_1.ASCII_CONTROL_CHARS.source, security_1.ASCII_CONTROL_CHARS.flags); const extendedPattern = new RegExp(security_1.EXTENDED_ASCII_CONTROL_CHARS.source, security_1.EXTENDED_ASCII_CONTROL_CHARS.flags); if (asciiPattern.test(url) || extendedPattern.test(url)) { return false; } return true; }