Browse Source

fix: prevent regex injection in promptConstructor and add error handling

- Replace RegExp constructor with replaceAll() for @variable substitution
  in promptConstructor to prevent regex injection via variable names
- Wrap annotation, prompt, and promptConstructor execution cases in
  try/catch to gracefully handle errors (log + set node error, continue)
- Fix MediaBunny resource cleanup type safety with closeable type guard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
4f13d76a55
  1. 10
      src/hooks/useAudioMixing.ts
  2. 126
      src/store/workflowStore.ts

10
src/hooks/useAudioMixing.ts

@ -80,13 +80,17 @@ async function decodeWithMediaBunny(
return decodedBuffers;
} finally {
if (sink && typeof sink.close === 'function') {
// Defensively close resources — types may not declare close() but implementations may have it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const closeable = (obj: unknown): obj is { close(): Promise<void> } =>
obj != null && typeof (obj as any).close === 'function';
if (closeable(sink)) {
try { await sink.close(); } catch (e) { console.warn('Failed to close sink:', e); }
}
if (input && typeof input.close === 'function') {
if (closeable(input)) {
try { await input.close(); } catch (e) { console.warn('Failed to close input:', e); }
}
if (blobSource && typeof blobSource.close === 'function') {
if (closeable(blobSource)) {
try { await blobSource.close(); } catch (e) { console.warn('Failed to close blobSource:', e); }
}
}

126
src/store/workflowStore.ts

@ -1166,75 +1166,93 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
break;
case "annotation": {
// Get connected image and set as source (use first image)
const { images } = getConnectedInputs(node.id);
const image = images[0] || null;
if (image) {
updateNodeData(node.id, { sourceImage: image });
// If no annotations, pass through the image
const nodeData = node.data as AnnotationNodeData;
if (!nodeData.outputImage) {
updateNodeData(node.id, { outputImage: image });
try {
// Get connected image and set as source (use first image)
const { images } = getConnectedInputs(node.id);
const image = images[0] || null;
if (image) {
updateNodeData(node.id, { sourceImage: image });
// If no annotations, pass through the image
const nodeData = node.data as AnnotationNodeData;
if (!nodeData.outputImage) {
updateNodeData(node.id, { outputImage: image });
}
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Workflow] Annotation node ${node.id} failed:`, message);
updateNodeData(node.id, { error: message });
}
break;
}
case "prompt": {
// Check for connected text input and update prompt if connected
const { text: connectedText } = getConnectedInputs(node.id);
if (connectedText !== null) {
updateNodeData(node.id, { prompt: connectedText });
try {
// Check for connected text input and update prompt if connected
const { text: connectedText } = getConnectedInputs(node.id);
if (connectedText !== null) {
updateNodeData(node.id, { prompt: connectedText });
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Workflow] Prompt node ${node.id} failed:`, message);
updateNodeData(node.id, { error: message });
}
break;
}
case "promptConstructor": {
// Get fresh node data from store
const freshNode = get().nodes.find((n) => n.id === node.id);
const nodeData = (freshNode?.data || node.data) as PromptConstructorNodeData;
const template = nodeData.template;
// Find connected prompt nodes via text edges
const connectedPromptNodes = edges
.filter((e) => e.target === node.id && e.targetHandle === "text")
.map((e) => nodes.find((n) => n.id === e.source))
.filter((n): n is WorkflowNode => n !== undefined && n.type === "prompt");
// Build variable map from connected prompt nodes
const variableMap: Record<string, string> = {};
connectedPromptNodes.forEach((promptNode) => {
const promptData = promptNode.data as PromptNodeData;
if (promptData.variableName) {
variableMap[promptData.variableName] = promptData.prompt;
}
});
try {
// Get fresh node data from store
const freshNode = get().nodes.find((n) => n.id === node.id);
const nodeData = (freshNode?.data || node.data) as PromptConstructorNodeData;
const template = nodeData.template;
// Find connected prompt nodes via text edges
const connectedPromptNodes = edges
.filter((e) => e.target === node.id && e.targetHandle === "text")
.map((e) => nodes.find((n) => n.id === e.source))
.filter((n): n is WorkflowNode => n !== undefined && n.type === "prompt");
// Build variable map from connected prompt nodes
const variableMap: Record<string, string> = {};
connectedPromptNodes.forEach((promptNode) => {
const promptData = promptNode.data as PromptNodeData;
if (promptData.variableName) {
variableMap[promptData.variableName] = promptData.prompt;
}
});
// Find all @variable patterns in template
const varPattern = /@(\w+)/g;
const unresolvedVars: string[] = [];
let resolvedText = template;
// Replace @variables with values or track unresolved
const matches = template.matchAll(varPattern);
for (const match of matches) {
const varName = match[1];
if (variableMap[varName] !== undefined) {
// Replace with actual value
resolvedText = resolvedText.replace(new RegExp(`@${varName}`, 'g'), variableMap[varName]);
} else {
// Track unresolved variable
if (!unresolvedVars.includes(varName)) {
unresolvedVars.push(varName);
// Find all @variable patterns in template
const varPattern = /@(\w+)/g;
const unresolvedVars: string[] = [];
let resolvedText = template;
// Replace @variables with values or track unresolved
const matches = template.matchAll(varPattern);
for (const match of matches) {
const varName = match[1];
if (variableMap[varName] !== undefined) {
// Replace with actual value
resolvedText = resolvedText.replaceAll(`@${varName}`, variableMap[varName]);
} else {
// Track unresolved variable
if (!unresolvedVars.includes(varName)) {
unresolvedVars.push(varName);
}
}
}
}
// Update node with resolved text and unresolved vars list
updateNodeData(node.id, {
outputText: resolvedText,
unresolvedVars,
});
// Update node with resolved text and unresolved vars list
updateNodeData(node.id, {
outputText: resolvedText,
unresolvedVars,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Workflow] PromptConstructor node ${node.id} failed:`, message);
updateNodeData(node.id, { error: message });
}
break;
}

Loading…
Cancel
Save