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:
Henry Heng
2025-07-19 01:11:31 +01:00
committed by GitHub
parent fbe9f34a60
commit 96a57a58e7
5 changed files with 536 additions and 12 deletions
@@ -2,8 +2,8 @@ import { getBaseClasses, INode, INodeData, INodeParams } from '../../../src'
import { BaseOutputParser } from '@langchain/core/output_parsers'
import { StructuredOutputParser as LangchainStructuredOutputParser } from 'langchain/output_parsers'
import { CATEGORY } from '../OutputParserHelpers'
import { z } from 'zod'
import { jsonrepair } from 'jsonrepair'
import { SecureZodSchemaParser } from '../../../src/secureZodParser'
class AdvancedStructuredOutputParser implements INode {
label: string
@@ -57,10 +57,8 @@ class AdvancedStructuredOutputParser implements INode {
const schemaString = nodeData.inputs?.exampleJson as string
const autoFix = nodeData.inputs?.autofixParser as boolean
const zodSchemaFunction = new Function('z', `return ${schemaString}`)
const zodSchema = zodSchemaFunction(z)
try {
const zodSchema = SecureZodSchemaParser.parseZodSchema(schemaString)
const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(zodSchema)
const baseParse = structuredOutputParser.parse
@@ -3,6 +3,7 @@ import { convertSchemaToZod, getBaseClasses, getVars } from '../../../src/utils'
import { DynamicStructuredTool } from './core'
import { z } from 'zod'
import { DataSource } from 'typeorm'
import { SecureZodSchemaParser } from '../../../src/secureZodParser'
class CustomTool_Tools implements INode {
label: string
@@ -119,8 +120,7 @@ class CustomTool_Tools implements INode {
if (customToolName) obj.name = customToolName
if (customToolDesc) obj.description = customToolDesc
if (customToolSchema) {
const zodSchemaFunction = new Function('z', `return ${customToolSchema}`)
obj.schema = zodSchemaFunction(z)
obj.schema = SecureZodSchemaParser.parseZodSchema(customToolSchema) as z.ZodObject<ICommonObject, 'strip', z.ZodTypeAny>
}
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
@@ -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
}
}
}