UNPKG

mcp-sanitizer

Version:

Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries

321 lines (280 loc) 9.19 kB
/** * String manipulation and validation utilities for MCP Sanitizer * * This module provides reusable functions for string sanitization, * validation, and manipulation used throughout the MCP Sanitizer. */ const escapeHtml = require('escape-html'); /** * HTML encode a string to prevent XSS attacks * @param {string} str - The string to encode * @returns {string} - HTML encoded string * @throws {Error} - If input is not a string */ function htmlEncode (str) { if (typeof str !== 'string') { throw new Error('Input must be a string'); } // Use escape-html library for better security and performance return escapeHtml(str); } /** * Check if a string exceeds the maximum allowed length * @param {string} str - The string to check * @param {number} maxLength - Maximum allowed length * @returns {boolean} - True if string is within length limit * @throws {Error} - If parameters are invalid */ function isWithinLengthLimit (str, maxLength) { if (typeof str !== 'string') { throw new Error('String parameter must be a string'); } if (typeof maxLength !== 'number' || maxLength < 0) { throw new Error('Max length must be a non-negative number'); } return str.length <= maxLength; } /** * Validate string length and throw error if exceeded * @param {string} str - The string to validate * @param {number} maxLength - Maximum allowed length * @throws {Error} - If string exceeds maximum length */ function validateStringLength (str, maxLength) { if (!isWithinLengthLimit(str, maxLength)) { throw new Error(`String exceeds maximum length of ${maxLength} characters`); } } /** * Check if a string contains any blocked patterns * @param {string} str - The string to check * @param {RegExp[]} patterns - Array of regex patterns to check against * @returns {RegExp|null} - The first matching pattern or null if none match * @throws {Error} - If parameters are invalid */ function findBlockedPattern (str, patterns) { if (typeof str !== 'string') { throw new Error('String parameter must be a string'); } if (!Array.isArray(patterns)) { throw new Error('Patterns must be an array'); } for (const pattern of patterns) { if (!(pattern instanceof RegExp)) { throw new Error('All patterns must be RegExp objects'); } if (pattern.test(str)) { return pattern; } } return null; } /** * Validate string against blocked patterns * @param {string} str - The string to validate * @param {RegExp[]} patterns - Array of regex patterns to check against * @throws {Error} - If string contains blocked patterns */ function validateAgainstBlockedPatterns (str, patterns, context = {}) { const matchedPattern = findBlockedPattern(str, patterns); if (matchedPattern) { // For SQL context with PostgreSQL dollar quotes, provide specific message if (context.type === 'sql' && str.includes('$$')) { // Check if it's actually PostgreSQL dollar quoting const dollarQuotePattern = /\$\$.*?\$\$/; if (dollarQuotePattern.test(str)) { throw new Error('PostgreSQL dollar quoting detected'); } } throw new Error(`String contains blocked pattern: ${matchedPattern}`); } } /** * Check if string contains SQL injection keywords * @param {string} str - The string to check * @param {string[]} keywords - Array of SQL keywords to check against * @returns {string|null} - The first matching keyword or null if none match * @throws {Error} - If parameters are invalid */ function findSQLKeyword (str, keywords) { if (typeof str !== 'string') { throw new Error('String parameter must be a string'); } if (!Array.isArray(keywords)) { throw new Error('Keywords must be an array'); } const upperStr = str.toUpperCase(); for (const keyword of keywords) { if (typeof keyword !== 'string') { throw new Error('All keywords must be strings'); } // Handle pattern keywords like 'SELECT.*FROM' if (keyword.includes('.*')) { const pattern = new RegExp(keyword.replace(/\.\*/g, '.*'), 'i'); if (pattern.test(str)) { return keyword; } } else { // Simple keyword matching if (upperStr.includes(keyword.toUpperCase())) { return keyword; } } } return null; } /** * Validate string against SQL injection keywords * @param {string} str - The string to validate * @param {string[]} keywords - Array of SQL keywords to check against * @throws {Error} - If string contains SQL keywords */ function validateAgainstSQLKeywords (str, keywords) { const matchedKeyword = findSQLKeyword(str, keywords); if (matchedKeyword) { throw new Error(`String contains potentially dangerous SQL keyword: ${matchedKeyword}`); } } /** * Safely trim a string, handling edge cases * @param {*} input - The input to trim (will be converted to string if possible) * @returns {string} - Trimmed string * @throws {Error} - If input cannot be converted to string */ function safeTrim (input) { if (input === null || input === undefined) { return ''; } if (typeof input !== 'string') { if (typeof input.toString === 'function') { input = input.toString(); } else { throw new Error('Input cannot be converted to string'); } } return input.trim(); } /** * Check if a string is empty or contains only whitespace * @param {string} str - The string to check * @returns {boolean} - True if string is empty or whitespace only */ function isEmpty (str) { if (typeof str !== 'string') { return false; } return str.trim().length === 0; } /** * Normalize line endings in a string to LF (\n) * @param {string} str - The string to normalize * @returns {string} - String with normalized line endings * @throws {Error} - If input is not a string */ function normalizeLineEndings (str) { if (typeof str !== 'string') { throw new Error('Input must be a string'); } return str.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); } /** * Escape special regex characters in a string * @param {string} str - The string to escape * @returns {string} - String with escaped regex characters * @throws {Error} - If input is not a string */ function escapeRegex (str) { if (typeof str !== 'string') { throw new Error('Input must be a string'); } return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Check if a string contains only safe characters (alphanumeric, spaces, common punctuation) * @param {string} str - The string to check * @param {RegExp} [allowedCharsPattern] - Custom pattern for allowed characters * @returns {boolean} - True if string contains only safe characters * @throws {Error} - If input is not a string */ function containsOnlySafeChars (str, allowedCharsPattern = /^[a-zA-Z0-9\s\-_.,!?()[\]{}:;"'@#$%^&*+=~`|\\/<>]*$/) { if (typeof str !== 'string') { throw new Error('Input must be a string'); } if (!(allowedCharsPattern instanceof RegExp)) { throw new Error('Allowed characters pattern must be a RegExp'); } return allowedCharsPattern.test(str); } // Import security enhancements const { detectDirectionalOverrides, detectNullBytes, handleEmptyStrings } = require('./security-enhancements'); /** * Enhanced string validation with security enhancements * @param {string} str - String to validate * @param {Object} options - Validation options * @returns {Object} Enhanced validation result */ function enhancedStringValidation (str, options = {}) { const { checkDirectionalOverrides = true, checkNullBytes = true, // checkMultipleEncoding = false, // Only for URLs - Unused handleEmpty = true, emptyContext = {} } = options; const results = { isValid: true, warnings: [], sanitized: str, metadata: {} }; // Check for directional overrides if (checkDirectionalOverrides) { const dirResult = detectDirectionalOverrides(str); if (dirResult.detected) { results.warnings.push(...dirResult.warnings); results.sanitized = dirResult.sanitized; results.metadata.directionalOverrides = dirResult.metadata; } } // Check for null bytes if (checkNullBytes) { const nullResult = detectNullBytes(results.sanitized); if (nullResult.detected) { results.warnings.push(...nullResult.warnings); results.sanitized = nullResult.sanitized; results.metadata.nullBytes = nullResult.metadata; } } // Handle empty strings if (handleEmpty) { const emptyResult = handleEmptyStrings(results.sanitized, emptyContext); if (!emptyResult.isValid) { results.isValid = false; results.warnings.push(...emptyResult.warnings); } if (emptyResult.processed !== results.sanitized) { results.sanitized = emptyResult.processed; } results.metadata.emptyString = emptyResult.metadata; } return results; } module.exports = { htmlEncode, isWithinLengthLimit, validateStringLength, findBlockedPattern, validateAgainstBlockedPatterns, findSQLKeyword, validateAgainstSQLKeywords, safeTrim, isEmpty, normalizeLineEndings, escapeRegex, containsOnlySafeChars, enhancedStringValidation };