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. 20
      src/store/workflowStore.ts

10
src/hooks/useAudioMixing.ts

@ -80,13 +80,17 @@ async function decodeWithMediaBunny(
return decodedBuffers; return decodedBuffers;
} finally { } 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); } 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); } 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); } try { await blobSource.close(); } catch (e) { console.warn('Failed to close blobSource:', e); }
} }
} }

20
src/store/workflowStore.ts

@ -1166,6 +1166,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
break; break;
case "annotation": { case "annotation": {
try {
// Get connected image and set as source (use first image) // Get connected image and set as source (use first image)
const { images } = getConnectedInputs(node.id); const { images } = getConnectedInputs(node.id);
const image = images[0] || null; const image = images[0] || null;
@ -1177,19 +1178,31 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
updateNodeData(node.id, { outputImage: image }); 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; break;
} }
case "prompt": { case "prompt": {
try {
// Check for connected text input and update prompt if connected // Check for connected text input and update prompt if connected
const { text: connectedText } = getConnectedInputs(node.id); const { text: connectedText } = getConnectedInputs(node.id);
if (connectedText !== null) { if (connectedText !== null) {
updateNodeData(node.id, { prompt: connectedText }); 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; break;
} }
case "promptConstructor": { case "promptConstructor": {
try {
// Get fresh node data from store // Get fresh node data from store
const freshNode = get().nodes.find((n) => n.id === node.id); const freshNode = get().nodes.find((n) => n.id === node.id);
const nodeData = (freshNode?.data || node.data) as PromptConstructorNodeData; const nodeData = (freshNode?.data || node.data) as PromptConstructorNodeData;
@ -1221,7 +1234,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const varName = match[1]; const varName = match[1];
if (variableMap[varName] !== undefined) { if (variableMap[varName] !== undefined) {
// Replace with actual value // Replace with actual value
resolvedText = resolvedText.replace(new RegExp(`@${varName}`, 'g'), variableMap[varName]); resolvedText = resolvedText.replaceAll(`@${varName}`, variableMap[varName]);
} else { } else {
// Track unresolved variable // Track unresolved variable
if (!unresolvedVars.includes(varName)) { if (!unresolvedVars.includes(varName)) {
@ -1235,6 +1248,11 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
outputText: resolvedText, outputText: resolvedText,
unresolvedVars, 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; break;
} }

Loading…
Cancel
Save