import { ExcalidrawValidationError } from "./errors.js";
// Regex to validate IDs - only allow alphanumeric characters, hyphens, and underscores
const ID_VALIDATION_REGEX = /^[a-zA-Z0-9\-_]+$/;
/**
* Validates and sanitizes a file ID to prevent path traversal attacks
* @param id The ID to validate
* @throws ExcalidrawValidationError if the ID is invalid
*/
export function validateFileId(id: string): void {
if (!id || typeof id !== "string") {
throw new ExcalidrawValidationError("ID must be a non-empty string");
}
if (!ID_VALIDATION_REGEX.test(id)) {
throw new ExcalidrawValidationError(
"ID contains invalid characters. Only alphanumeric characters, hyphens, and underscores are allowed"
);
}
// Additional checks for common path traversal patterns
if (id.includes("..") || id.includes("/") || id.includes("\\")) {
throw new ExcalidrawValidationError(
"ID contains forbidden path characters"
);
}
// Prevent IDs that are too long (DoS protection)
if (id.length > 100) {
throw new ExcalidrawValidationError(
"ID is too long (maximum 100 characters)"
);
}
}
/**
* Safely parses JSON with proper error handling
* @param jsonString The JSON string to parse
* @param context Context for error messages (e.g., "metadata", "drawing content")
* @returns Parsed JSON object
* @throws ExcalidrawValidationError if JSON is invalid
*/
export function safeJsonParse(
jsonString: string,
context: string = "JSON"
): any {
try {
return JSON.parse(jsonString);
} catch (error) {
throw new ExcalidrawValidationError(
`Invalid ${context} format: ${
error instanceof Error ? error.message : "Unknown parsing error"
}`
);
}
}
/**
* Sanitizes error messages to prevent information disclosure
* @param error The original error
* @param context Optional context for the error
* @returns Sanitized error message
*/
export function sanitizeErrorMessage(error: any, context?: string): string {
if (!error) {
return "An unknown error occurred";
}
let message = error.message || "An error occurred";
// Remove potential file paths
message = message.replace(/\/[^\s]*\//g, "[path]");
message = message.replace(/\\[^\s]*\\/g, "[path]");
// Remove potential system information
message = message.replace(/ENOENT|EACCES|EPERM|EISDIR/g, "file access error");
if (context) {
return `${context}: ${message}`;
}
return message;
}