diff --git a/src/systems/interaction/inventoryActions.js b/src/systems/interaction/inventoryActions.js index 44ce3c0..3038764 100644 --- a/src/systems/interaction/inventoryActions.js +++ b/src/systems/interaction/inventoryActions.js @@ -8,6 +8,7 @@ import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/p import { buildInventorySummary } from '../generation/promptBuilder.js'; import { renderInventory } from '../rendering/inventory.js'; import { parseItems, serializeItems } from '../../utils/itemParser.js'; +import { sanitizeLocationName, sanitizeItemName } from '../../utils/security.js'; // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ @@ -139,9 +140,17 @@ export function saveAddItem(field, location) { } const input = $(inputId); - const itemName = input.val().trim(); + const rawItemName = input.val().trim(); + if (!rawItemName) { + hideAddItemForm(field, location); + return; + } + + // Security: Validate and sanitize item name + const itemName = sanitizeItemName(rawItemName); if (!itemName) { + alert('Invalid item name.'); hideAddItemForm(field, location); return; } @@ -246,9 +255,17 @@ export function hideAddLocationForm() { export function saveAddLocation() { const inventory = extensionSettings.userStats.inventory; const input = $('#rpg-new-location-name'); - const locationName = input.val().trim(); + const rawLocationName = input.val().trim(); + if (!rawLocationName) { + hideAddLocationForm(); + return; + } + + // Security: Validate and sanitize location name + const locationName = sanitizeLocationName(rawLocationName); if (!locationName) { + alert('Invalid location name. Avoid special names like "__proto__" or "constructor".'); hideAddLocationForm(); return; } diff --git a/src/utils/itemParser.js b/src/utils/itemParser.js index 42679e9..4351312 100644 --- a/src/utils/itemParser.js +++ b/src/utils/itemParser.js @@ -3,6 +3,8 @@ * Utilities for parsing item strings into arrays and vice versa */ +import { sanitizeItemName, MAX_ITEMS_PER_SECTION } from './security.js'; + /** * Parses item strings from AI responses into clean arrays. * Handles numerous AI formatting quirks and edge cases. @@ -124,7 +126,7 @@ export function parseItems(itemString) { processed = processed.replace(/\s+/g, ' '); // STEP 6: Smart comma splitting (only split on commas OUTSIDE parentheses) - // Also handles list markers and quotes per-item + // Also handles list markers, quotes, and security validation per-item const items = []; let currentItem = ''; parenDepth = 0; @@ -147,7 +149,17 @@ export function parseItems(itemString) { // Comma outside parentheses - this is a separator const cleaned = cleanSingleItem(currentItem); if (cleaned) { - items.push(cleaned); + // Security check: validate and sanitize item name + const sanitized = sanitizeItemName(cleaned); + if (sanitized) { + items.push(sanitized); + } + + // DoS protection: enforce max items limit + if (items.length >= MAX_ITEMS_PER_SECTION) { + console.warn(`[RPG Companion] Reached max items limit (${MAX_ITEMS_PER_SECTION}), truncating list`); + return items; + } } currentItem = ''; // Start new item } else { @@ -158,7 +170,11 @@ export function parseItems(itemString) { // Don't forget the last item const cleaned = cleanSingleItem(currentItem); if (cleaned) { - items.push(cleaned); + // Security check: validate and sanitize item name + const sanitized = sanitizeItemName(cleaned); + if (sanitized) { + items.push(sanitized); + } } // Warn if parentheses were unmatched diff --git a/src/utils/security.js b/src/utils/security.js new file mode 100644 index 0000000..17f7ad5 --- /dev/null +++ b/src/utils/security.js @@ -0,0 +1,158 @@ +/** + * Security Utilities Module + * Handles input sanitization and validation to prevent security vulnerabilities + */ + +/** + * List of dangerous property names that could cause prototype pollution + * or shadow critical object methods. + * @private + */ +const BLOCKED_PROPERTY_NAMES = [ + '__proto__', + 'constructor', + 'prototype', + 'toString', + 'valueOf', + 'hasOwnProperty', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__' +]; + +/** + * Validates and sanitizes storage location names. + * Prevents prototype pollution and object property shadowing attacks. + * + * @param {string} name - Location name to validate + * @returns {string|null} Sanitized location name or null if invalid/dangerous + * + * @example + * sanitizeLocationName("Home") // "Home" + * sanitizeLocationName("__proto__") // null (blocked, logs warning) + * sanitizeLocationName("A".repeat(300)) // "AAA..." (truncated to 200 chars) + */ +export function sanitizeLocationName(name) { + if (!name || typeof name !== 'string') { + return null; + } + + const trimmed = name.trim(); + + // Empty check + if (trimmed === '') { + return null; + } + + // Check for dangerous property names (case-insensitive) + const lowerName = trimmed.toLowerCase(); + if (BLOCKED_PROPERTY_NAMES.some(blocked => lowerName === blocked.toLowerCase())) { + console.warn(`[RPG Companion] Blocked dangerous location name: "${trimmed}"`); + return null; + } + + // Max length check (reasonable location name) + const MAX_LOCATION_LENGTH = 200; + if (trimmed.length > MAX_LOCATION_LENGTH) { + console.warn(`[RPG Companion] Location name too long (${trimmed.length} chars), truncating to ${MAX_LOCATION_LENGTH}`); + return trimmed.slice(0, MAX_LOCATION_LENGTH); + } + + return trimmed; +} + +/** + * Validates and sanitizes item names. + * Prevents excessively long item names that could cause DoS or UI issues. + * + * @param {string} name - Item name to validate + * @returns {string|null} Sanitized item name or null if invalid + * + * @example + * sanitizeItemName("Sword") // "Sword" + * sanitizeItemName("") // null + * sanitizeItemName("A".repeat(600)) // "AAA..." (truncated to 500 chars) + */ +export function sanitizeItemName(name) { + if (!name || typeof name !== 'string') { + return null; + } + + const trimmed = name.trim(); + + // Empty check + if (trimmed === '' || trimmed.toLowerCase() === 'none') { + return null; + } + + // Max length check (reasonable item name with description) + const MAX_ITEM_LENGTH = 500; + if (trimmed.length > MAX_ITEM_LENGTH) { + console.warn(`[RPG Companion] Item name too long (${trimmed.length} chars), truncating to ${MAX_ITEM_LENGTH}`); + return trimmed.slice(0, MAX_ITEM_LENGTH); + } + + return trimmed; +} + +/** + * Validates and cleans a stored inventory object. + * Ensures all keys are safe property names and all values are strings. + * Prevents prototype pollution attacks via object keys. + * + * @param {Object} stored - Raw stored inventory object + * @returns {Object} Cleaned stored inventory object (always a plain object) + * + * @example + * validateStoredInventory({ "Home": "Sword, Shield" }) + * // → { "Home": "Sword, Shield" } + * + * validateStoredInventory({ "__proto__": "malicious" }) + * // → {} (dangerous key removed, logged) + * + * validateStoredInventory(null) + * // → {} (invalid input, returns empty object) + */ +export function validateStoredInventory(stored) { + // Handle invalid input + if (!stored || typeof stored !== 'object' || Array.isArray(stored)) { + return {}; + } + + const cleaned = {}; + + // Validate each property + for (const key in stored) { + // Only check own properties (not inherited) + if (!Object.prototype.hasOwnProperty.call(stored, key)) { + continue; + } + + // Sanitize the location name + const sanitizedKey = sanitizeLocationName(key); + if (!sanitizedKey) { + // Key was invalid or dangerous, skip it + continue; + } + + // Ensure value is a string + const value = stored[key]; + if (typeof value !== 'string') { + console.warn(`[RPG Companion] Invalid stored inventory value for location "${sanitizedKey}", skipping`); + continue; + } + + // Add to cleaned object + cleaned[sanitizedKey] = value; + } + + return cleaned; +} + +/** + * Maximum number of items allowed in a single inventory section. + * Prevents DoS via extremely large item lists. + * @constant {number} + */ +export const MAX_ITEMS_PER_SECTION = 500;