Files
jellytau/src/lib/utils/validation.ts
T

124 lines
3.4 KiB
TypeScript

/**
* 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");
}
}