From fd8780ae271c95ee1a67c7588fc63115ebfe5526 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Fri, 10 Jan 2025 03:34:44 +1030 Subject: [PATCH] feat: implement comprehensive resource name and path validation to prevent conflicts and ensure safe routing --- frontend/src/api/data.js | 207 ++++++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 37 deletions(-) diff --git a/frontend/src/api/data.js b/frontend/src/api/data.js index 561ccc2..0c12da8 100644 --- a/frontend/src/api/data.js +++ b/frontend/src/api/data.js @@ -2,66 +2,225 @@ import axios from 'axios'; const BASE_URL = '/api/data'; +// Define all special endpoints that could conflict with resource names +const SPECIAL_ENDPOINTS = [ + 'test', + 'validate', + 'search', + 'batch', + 'export', + 'import', + 'stats', + 'metrics', + 'health', + 'status', + 'config', + 'settings', + 'logs', + 'audit', + 'backup', + 'restore', + 'sync', + 'preview', + 'publish', + 'deploy', + 'run', + 'execute', + 'process', + 'analyze', + 'verify', + 'check' +]; + +// Define characters and patterns that could cause routing issues +const UNSAFE_PATTERNS = [ + /[\/\\]/, // No slashes (forward or backward) + /[\s]/, // No whitespace + /[<>:"|?*]/, // No special characters that might be interpreted by the system + /^\.+/, // No dots at start (prevent relative paths) + /\.+$/, // No dots at end + /^-/, // No dash at start + /-$/, // No dash at end + /--|__|\.\./ // No double separators +]; + +// Comprehensive name validation +const validateResourceName = (category, name) => { + // Basic checks + if (!name || typeof name !== 'string') { + throw new Error('Resource name must be a non-empty string'); + } + + // Length check + if (name.length < 1 || name.length > 64) { + throw new Error('Resource name must be between 1 and 64 characters'); + } + + // Check for special endpoints + if (SPECIAL_ENDPOINTS.includes(name.toLowerCase())) { + throw new Error( + `'${name}' is a reserved word and cannot be used as a resource name` + ); + } + + // Check for unsafe patterns + for (const pattern of UNSAFE_PATTERNS) { + if (pattern.test(name)) { + throw new Error( + 'Resource name contains invalid characters or patterns' + ); + } + } + + // Only allow alphanumeric characters, single hyphens, and underscores + const validNamePattern = /^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/; + if (!validNamePattern.test(name)) { + throw new Error( + 'Resource name must contain only letters, numbers, hyphens, and underscores, and cannot have consecutive separators' + ); + } + + return true; +}; + +// Validate entire path to protect against path traversal +const validatePath = parts => { + if (!Array.isArray(parts)) { + throw new Error('Path must be an array of segments'); + } + + const joinedPath = parts.join('/'); + + // Prevent any path traversal attempts + if ( + joinedPath.includes('..') || + joinedPath.includes('./') || + joinedPath.includes('/.') + ) { + throw new Error('Invalid path detected'); + } + + // Ensure path doesn't start or end with separators + if (joinedPath.startsWith('/') || joinedPath.endsWith('/')) { + throw new Error('Path cannot start or end with separators'); + } + + return true; +}; + const handleError = (error, operation) => { console.error(`Error ${operation}:`, error); - const errorMessage = - error.response?.data?.error || `Failed to ${operation}`; - throw new Error(errorMessage); // Throw instead of returning an error object + + // If it's already an Error object with a message, throw it directly + if (error instanceof Error) { + throw error; + } + + // If it's an axios error with a response + if (error.response?.data) { + const errorData = error.response.data; + // Handle various error response formats + const message = + errorData.error || + errorData.message || + errorData.detail || + (typeof errorData === 'string' ? errorData : null); + + if (message) { + throw new Error(message); + } + } + + // Fallback generic error + throw new Error(`Failed to ${operation}`); }; // Get all items for a category export const getAllItems = async category => { try { + validateResourceName('category', category); + validatePath([category]); const response = await axios.get(`${BASE_URL}/${category}`); return response.data; } catch (error) { - return handleError(error, `fetch ${category} items`); + throw handleError(error, `fetch ${category} items`); } }; // Get single item export const getItem = async (category, name) => { try { + validateResourceName('category', category); + validateResourceName(category, name); + validatePath([category, name]); const response = await axios.get(`${BASE_URL}/${category}/${name}`); return response.data; } catch (error) { - return handleError(error, `fetch ${category} item ${name}`); + throw handleError(error, `fetch ${category} item ${name}`); } }; // Create new item export const createItem = async (category, data) => { try { + validateResourceName('category', category); + validateResourceName(category, data.name); + validatePath([category, data.name]); const response = await axios.post( `${BASE_URL}/${category}/${data.name}`, data ); return response.data; } catch (error) { - return handleError(error, `create ${category} item`); + throw handleError(error, `create ${category} item`); } }; // Update existing item export const updateItem = async (category, name, data, newName) => { try { + validateResourceName('category', category); + validateResourceName(category, name); + if (newName) { + validateResourceName(category, newName); + validatePath([category, newName]); + } + validatePath([category, name]); const response = await axios.put(`${BASE_URL}/${category}/${name}`, { ...data, - ...(newName && {rename: newName}) // Only add rename field if newName exists + ...(newName && {rename: newName}) }); return response.data; } catch (error) { - return handleError(error, `update ${category} item ${name}`); + throw handleError(error, `update ${category} item ${name}`); } }; // Delete item export const deleteItem = async (category, name) => { try { + validateResourceName('category', category); + validateResourceName(category, name); + validatePath([category, name]); const response = await axios.delete(`${BASE_URL}/${category}/${name}`); return response.data; } catch (error) { - return handleError(error, `delete ${category} item ${name}`); + throw handleError(error, `delete ${category} item ${name}`); + } +}; + +// Special endpoint wrapper +const createSpecialEndpoint = (category, endpoint) => async data => { + try { + validateResourceName('category', category); + validatePath([category, endpoint]); + const response = await axios.post( + `${BASE_URL}/${category}/${endpoint}`, + data + ); + return response.data; + } catch (error) { + throw handleError(error, `execute ${category} ${endpoint}`); } }; @@ -80,20 +239,7 @@ export const CustomFormats = { update: (name, data, newName) => updateItem('custom_format', name, data, newName), delete: name => deleteItem('custom_format', name), - runTests: async ({conditions, tests}) => { - try { - const response = await axios.post( - `${BASE_URL}/custom_format/test`, - { - conditions, - tests - } - ); - return response.data; - } catch (error) { - return handleError(error, 'run tests'); - } - } + runTests: createSpecialEndpoint('custom_format', 'test') }; export const RegexPatterns = { @@ -103,18 +249,5 @@ export const RegexPatterns = { update: (name, data, newName) => updateItem('regex_pattern', name, data, newName), delete: name => deleteItem('regex_pattern', name), - runTests: async (pattern, tests) => { - try { - const response = await axios.post( - `${BASE_URL}/regex_pattern/test`, - { - pattern, - tests - } - ); - return response.data; - } catch (error) { - return handleError(error, 'run tests'); - } - } + runTests: createSpecialEndpoint('regex_pattern', 'test') };