Skip to main content
Glama
input-sanitizer.ts6.27 kB
import DOMPurify from 'isomorphic-dompurify'; export interface SanitizationOptions { allowedTags?: string[]; allowedAttr?: string[]; allowDataAttr?: boolean; allowUnknownProtocols?: boolean; stripAllHtml?: boolean; allowBasicFormatting?: boolean; allowLinks?: boolean; } /** * Sanitizes a string to prevent XSS attacks. * It allows a safe subset of HTML tags for basic formatting. * * @param input The string or other value to sanitize. * @param options Sanitization options. * @returns A sanitized string. */ export function sanitizeString(input: any, options: SanitizationOptions = {}): string { if (input === null || input === undefined) { return ''; } let stringifiedInput = String(input); // Manually remove javascript protocol to be safe stringifiedInput = stringifiedInput.replace(/javascript:/gi, ''); let allowedTags = options.allowedTags ?? []; if (options.allowBasicFormatting) { allowedTags = [...new Set([...allowedTags, 'b', 'i', 'u', 'strong', 'em', 'br', 'p'])]; } if (options.allowLinks) { allowedTags = [...new Set([...allowedTags, 'a'])]; } let allowedAttr = options.allowedAttr ?? []; if (options.allowLinks) { allowedAttr = [...new Set([...allowedAttr, 'href', 'title', 'target', 'rel'])]; } const domPurifyOptions: DOMPurify.Config = { ALLOWED_TAGS: options.stripAllHtml ? [] : allowedTags, ALLOWED_ATTR: options.stripAllHtml ? [] : allowedAttr, ALLOW_DATA_ATTR: options.allowDataAttr ?? false, ALLOW_UNKNOWN_PROTOCOLS: options.allowUnknownProtocols ?? false, }; const sanitized = DOMPurify.sanitize(stringifiedInput, domPurifyOptions); // Add a hook to enforce secure links if (options.allowLinks) { DOMPurify.addHook('afterSanitizeAttributes', (node) => { if (node.tagName === 'A' && node.hasAttribute('href')) { node.setAttribute('rel', 'noopener noreferrer'); node.setAttribute('target', '_blank'); } }); } const final = DOMPurify.sanitize(sanitized, domPurifyOptions); if (options.allowLinks) { DOMPurify.removeHook('afterSanitizeAttributes'); } return final; } /** * Recursively sanitizes an object by applying the `sanitizeString` function to all its string values. * * @param obj The object to sanitize. * @param options Sanitization options. * @returns A new object with all string values sanitized. */ export function sanitizeObject<T extends Record<string, any>>(obj: T, options: SanitizationOptions = {}): T { if (!obj || typeof obj !== 'object') { return obj; } const sanitizedObj = { ...obj }; for (const key in sanitizedObj) { const value = sanitizedObj[key]; if (typeof value === 'string') { sanitizedObj[key] = sanitizeString(value, options) as T[Extract<keyof T, string>]; } else if (typeof value === 'object') { sanitizedObj[key] = sanitizeObject(value, options); } } return sanitizedObj; } /** * Sanitizes an array by applying sanitization to its elements. * * @param arr The array to sanitize. * @param options Sanitization options. * @returns A new array with sanitized elements. */ export function sanitizeArray(arr: any[], options: SanitizationOptions = {}): any[] { if (!Array.isArray(arr)) { return []; } return arr.map(item => { if (typeof item === 'string') { return sanitizeString(item, options); } if (typeof item === 'object' && item !== null) { return sanitizeObject(item, options); } return item; }); } /** * Validates that a string contains only safe characters for specific use cases */ export class InputValidator { /** * Validates that a string is safe for use as an ID (alphanumeric + hyphens/underscores) */ static isValidId(input: string): boolean { return /^[a-zA-Z0-9_-]+$/.test(input); } /** * Validates that a string is a safe email format */ static isValidEmail(input: string): boolean { const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; return emailRegex.test(input); } /** * Validates that a string is safe for use as a URL */ static isValidUrl(input: string): boolean { try { const url = new URL(input); return ['http:', 'https:'].includes(url.protocol); } catch { return false; } } /** * Validates that a string contains no potentially dangerous characters */ static containsOnlySafeChars(input: string): boolean { // Allow letters, numbers, spaces, and basic punctuation return /^[a-zA-Z0-9\s.,!?;:()\-_"']+$/.test(input); } /** * Checks if string contains potential SQL injection patterns */ static containsSqlInjectionPatterns(input: string): boolean { const sqlPatterns = [ /union\s+select/i, /insert\s+into/i, /delete\s+from/i, /update\s+.*\s+set/i, /drop\s+table/i, /--/, /\/\*/, /\*\//, /;\s*$/, ]; return sqlPatterns.some(pattern => pattern.test(input)); } /** * Checks if string contains potential XSS patterns */ static containsXssPatterns(input: string): boolean { const xssPatterns = [ /<script/i, /javascript:/i, /on\w+\s*=/i, // onclick, onload, etc. /<iframe/i, /<object/i, /<embed/i, /vbscript:/i, ]; return xssPatterns.some(pattern => pattern.test(input)); } } /** * Sanitization presets for different use cases */ export const SanitizationPresets = { /** Strict sanitization - strips all HTML and special characters */ STRICT: { allowBasicFormatting: false, allowLinks: false, stripAllHtml: true, }, /** Form input sanitization - allows basic text formatting */ FORM_INPUT: { allowBasicFormatting: true, allowLinks: false, stripAllHtml: false, }, /** Rich text sanitization - allows formatting and links */ RICH_TEXT: { allowBasicFormatting: true, allowLinks: true, stripAllHtml: false, }, /** API parameter sanitization - very strict */ API_PARAM: { stripAllHtml: true, allowBasicFormatting: false, allowLinks: false, }, } as const;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server