mcp-sanitizer
Version:
Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries
247 lines (213 loc) • 6.96 kB
JavaScript
/**
* 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) {
const matchedPattern = findBlockedPattern(str, patterns)
if (matchedPattern) {
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)
}
module.exports = {
htmlEncode,
isWithinLengthLimit,
validateStringLength,
findBlockedPattern,
validateAgainstBlockedPatterns,
findSQLKeyword,
validateAgainstSQLKeywords,
safeTrim,
isEmpty,
normalizeLineEndings,
escapeRegex,
containsOnlySafeChars
}