124 lines
3.4 KiB
TypeScript
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");
|
|
}
|
|
}
|