mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
Refractor/SecureZodSchemaParser (#4898)
* refactor: Implement SecureZodSchemaParser for safe Zod schema handling and add FilterParser for Supabase filters * Replaced direct Zod schema evaluation with SecureZodSchemaParser in StructuredOutputParserAdvanced and CustomTool. * Introduced FilterParser to safely handle Supabase filter strings, preventing arbitrary code execution. * Added new filterParser.ts file to encapsulate filter parsing logic. * Updated Supabase vector store to utilize the new FilterParser for RPC filters. * Created secureZodParser.ts for secure parsing of Zod schemas. * remove console log
This commit is contained in:
@@ -3,11 +3,12 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Document } from '@langchain/core/documents'
|
||||
import { Embeddings } from '@langchain/core/embeddings'
|
||||
import { SupabaseVectorStore, SupabaseLibArgs, SupabaseFilterRPCCall } from '@langchain/community/vectorstores/supabase'
|
||||
import { SupabaseVectorStore, SupabaseLibArgs } from '@langchain/community/vectorstores/supabase'
|
||||
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
|
||||
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
|
||||
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
|
||||
import { index } from '../../../src/indexing'
|
||||
import { FilterParser } from './filterParser'
|
||||
|
||||
class Supabase_VectorStores implements INode {
|
||||
label: string
|
||||
@@ -233,11 +234,7 @@ class Supabase_VectorStores implements INode {
|
||||
}
|
||||
|
||||
if (supabaseRPCFilter) {
|
||||
const funcString = `return rpc.${supabaseRPCFilter};`
|
||||
const funcFilter = new Function('rpc', funcString)
|
||||
obj.filter = (rpc: SupabaseFilterRPCCall) => {
|
||||
return funcFilter(rpc)
|
||||
}
|
||||
obj.filter = FilterParser.parseFilterString(supabaseRPCFilter)
|
||||
}
|
||||
|
||||
const vectorStore = await SupabaseVectorStore.fromExistingIndex(embeddings, obj)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* This parser safely handles Supabase filter strings without allowing arbitrary code execution
|
||||
*/
|
||||
export class FilterParser {
|
||||
private static readonly ALLOWED_METHODS = ['filter', 'order', 'limit', 'range', 'single', 'maybeSingle']
|
||||
private static readonly ALLOWED_OPERATORS = [
|
||||
'eq',
|
||||
'neq',
|
||||
'gt',
|
||||
'gte',
|
||||
'lt',
|
||||
'lte',
|
||||
'like',
|
||||
'ilike',
|
||||
'is',
|
||||
'in',
|
||||
'cs',
|
||||
'cd',
|
||||
'sl',
|
||||
'sr',
|
||||
'nxl',
|
||||
'nxr',
|
||||
'adj',
|
||||
'ov',
|
||||
'fts',
|
||||
'plfts',
|
||||
'phfts',
|
||||
'wfts'
|
||||
]
|
||||
|
||||
/**
|
||||
* Safely parse a Supabase RPC filter string into a function
|
||||
* @param filterString The filter string (e.g., 'filter("metadata->a::int", "gt", 5).filter("metadata->c::int", "gt", 7)')
|
||||
* @returns A function that can be applied to an RPC object
|
||||
* @throws Error if the filter string contains unsafe patterns
|
||||
*/
|
||||
static parseFilterString(filterString: string): (rpc: any) => any {
|
||||
try {
|
||||
// Clean and validate the filter string
|
||||
const cleanedFilter = this.cleanFilterString(filterString)
|
||||
|
||||
// Parse the filter chain
|
||||
const filterChain = this.parseFilterChain(cleanedFilter)
|
||||
|
||||
// Build the safe filter function
|
||||
return this.buildFilterFunction(filterChain)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse Supabase filter: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private static cleanFilterString(filter: string): string {
|
||||
// Remove comments and normalize whitespace
|
||||
filter = filter.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
filter = filter.replace(/\s+/g, ' ').trim()
|
||||
|
||||
// Remove trailing semicolon if present
|
||||
if (filter.endsWith(';')) {
|
||||
filter = filter.slice(0, -1).trim()
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
private static parseFilterChain(filter: string): Array<{ method: string; args: any[] }> {
|
||||
const chain: Array<{ method: string; args: any[] }> = []
|
||||
|
||||
// Split on method calls (e.g., .filter, .order, etc.)
|
||||
const methodPattern = /\.?(\w+)\s*\((.*?)\)(?=\s*(?:\.|$))/g
|
||||
let match
|
||||
|
||||
while ((match = methodPattern.exec(filter)) !== null) {
|
||||
const method = match[1]
|
||||
const argsString = match[2]
|
||||
|
||||
// Validate method name
|
||||
if (!this.ALLOWED_METHODS.includes(method)) {
|
||||
throw new Error(`Disallowed method: ${method}`)
|
||||
}
|
||||
|
||||
// Parse arguments safely
|
||||
const args = this.parseArguments(argsString)
|
||||
|
||||
// Additional validation for filter method
|
||||
if (method === 'filter' && args.length >= 2) {
|
||||
const operator = args[1]
|
||||
if (typeof operator === 'string' && !this.ALLOWED_OPERATORS.includes(operator)) {
|
||||
throw new Error(`Disallowed filter operator: ${operator}`)
|
||||
}
|
||||
}
|
||||
|
||||
chain.push({ method, args })
|
||||
}
|
||||
|
||||
if (chain.length === 0) {
|
||||
throw new Error('No valid filter methods found')
|
||||
}
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
private static parseArguments(argsString: string): any[] {
|
||||
if (!argsString.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const args: any[] = []
|
||||
let current = ''
|
||||
let inString = false
|
||||
let stringChar = ''
|
||||
let depth = 0
|
||||
|
||||
for (let i = 0; i < argsString.length; i++) {
|
||||
const char = argsString[i]
|
||||
|
||||
if (!inString && (char === '"' || char === "'")) {
|
||||
inString = true
|
||||
stringChar = char
|
||||
current += char
|
||||
} else if (inString && char === stringChar && argsString[i - 1] !== '\\') {
|
||||
inString = false
|
||||
current += char
|
||||
} else if (!inString) {
|
||||
if (char === '(' || char === '[' || char === '{') {
|
||||
depth++
|
||||
current += char
|
||||
} else if (char === ')' || char === ']' || char === '}') {
|
||||
depth--
|
||||
current += char
|
||||
} else if (char === ',' && depth === 0) {
|
||||
args.push(this.parseArgument(current.trim()))
|
||||
current = ''
|
||||
continue
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
args.push(this.parseArgument(current.trim()))
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
private static parseArgument(arg: string): any {
|
||||
arg = arg.trim()
|
||||
|
||||
// Handle strings
|
||||
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
|
||||
return arg.slice(1, -1)
|
||||
}
|
||||
|
||||
// Handle numbers
|
||||
if (arg.match(/^-?\d+(\.\d+)?$/)) {
|
||||
return parseFloat(arg)
|
||||
}
|
||||
|
||||
// Handle booleans
|
||||
if (arg === 'true') return true
|
||||
if (arg === 'false') return false
|
||||
if (arg === 'null') return null
|
||||
|
||||
// Handle arrays (basic support)
|
||||
if (arg.startsWith('[') && arg.endsWith(']')) {
|
||||
const arrayContent = arg.slice(1, -1).trim()
|
||||
if (!arrayContent) return []
|
||||
|
||||
// Simple array parsing - just split by comma and parse each element
|
||||
return arrayContent.split(',').map((item) => this.parseArgument(item.trim()))
|
||||
}
|
||||
|
||||
// For everything else, treat as string (but validate it doesn't contain dangerous characters)
|
||||
if (arg.includes('require') || arg.includes('process') || arg.includes('eval') || arg.includes('Function')) {
|
||||
throw new Error(`Potentially dangerous argument: ${arg}`)
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
private static buildFilterFunction(chain: Array<{ method: string; args: any[] }>): (rpc: any) => any {
|
||||
return (rpc: any) => {
|
||||
let result = rpc
|
||||
|
||||
for (const { method, args } of chain) {
|
||||
if (typeof result[method] !== 'function') {
|
||||
throw new Error(`Method ${method} is not available on the RPC object`)
|
||||
}
|
||||
|
||||
try {
|
||||
result = result[method](...args)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to call ${method}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user