/** * Input validation utilities for security and data integrity */ /** * Validate Jellyfin item ID format * Item IDs should be non-empty alphanumeric strings with optional dashes/underscores */ export function validateItemId(itemId: string): void { if (!itemId || typeof itemId !== "string") { throw new Error("Invalid itemId: must be a non-empty string"); } if (itemId.length > 50) { throw new Error("Invalid itemId: exceeds maximum length of 50 characters"); } // Jellyfin item IDs are typically UUIDs or numeric IDs if (!/^[a-zA-Z0-9\-_]+$/.test(itemId)) { throw new Error("Invalid itemId: contains invalid characters"); } } /** * Validate image type to prevent path traversal attacks */ export function validateImageType(imageType: string): void { if (!imageType || typeof imageType !== "string") { throw new Error("Invalid imageType: must be a non-empty string"); } // Only allow known image types const validImageTypes = [ "Primary", "Backdrop", "Banner", "Disc", "Box", "Logo", "Thumb", "Art", "Chapter", "Keyframe", ]; if (!validImageTypes.includes(imageType)) { throw new Error(`Invalid imageType: "${imageType}" is not a valid image type`); } } /** * Validate media source ID format */ export function validateMediaSourceId(mediaSourceId: string): void { if (!mediaSourceId || typeof mediaSourceId !== "string") { throw new Error("Invalid mediaSourceId: must be a non-empty string"); } if (mediaSourceId.length > 50) { throw new Error("Invalid mediaSourceId: exceeds maximum length"); } if (!/^[a-zA-Z0-9\-_]+$/.test(mediaSourceId)) { throw new Error("Invalid mediaSourceId: contains invalid characters"); } } /** * Validate URL path segment to prevent directory traversal * Disallows: "..", ".", and characters that could enable attacks */ export function validateUrlPathSegment(segment: string): void { if (!segment || typeof segment !== "string") { throw new Error("Invalid path segment: must be a non-empty string"); } if (segment === ".." || segment === ".") { throw new Error("Invalid path segment: directory traversal not allowed"); } // Reject path separators and null bytes if (/[\/\\%]/.test(segment)) { throw new Error("Invalid path segment: contains invalid characters"); } } /** * Validate numeric parameter (width, height, quality, etc.) */ export function validateNumericParam(value: unknown, min = 0, max = 10000, name = "parameter"): number { // Must be an actual number, not a string that looks like a number if (typeof value !== "number") { throw new Error(`Invalid ${name}: must be an integer`); } if (!Number.isInteger(value)) { throw new Error(`Invalid ${name}: must be an integer`); } if (value < min || value > max) { throw new Error(`Invalid ${name}: must be between ${min} and ${max}`); } return value; } /** * Sanitize query parameter value - allows alphanumeric, dash, underscore */ export function validateQueryParamValue(value: string, maxLength = 100): void { if (typeof value !== "string") { throw new Error("Query parameter value must be a string"); } if (value.length > maxLength) { throw new Error(`Query parameter exceeds maximum length of ${maxLength}`); } // Allow only safe characters in query params if (!/^[a-zA-Z0-9\-_.~]+$/.test(value)) { throw new Error("Query parameter contains invalid characters"); } }