@nanggo/social-preview
Version:
Generate beautiful social media preview images from any URL
98 lines (97 loc) • 4.36 kB
JavaScript
;
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;
}