fix(execute-command): address review feedback — double-substitution, timeout:0, route path

- Rewrite resolveTagVariables to use index-based back-to-front splicing instead of
  global regex replace, preventing double-substitution when resolved values contain
  tag-like patterns
- Fix timeout:0 bypass by using explicit lower-bound check instead of destructuring default
- Add shell injection warning to bestPractices documentation
- Move API route from api/execute-command/ to api/tools/execute-command/ for consistency
This commit is contained in:
Waleed Latif
2026-03-05 13:03:25 -08:00
parent 2a8aae68c5
commit cf67966e15
3 changed files with 17 additions and 8 deletions

View File

@@ -98,16 +98,16 @@ function resolveTagVariables(
blockNameMapping: Record<string, string>,
blockOutputSchemas: Record<string, OutputSchema>
): string {
let resolved = command
const tagPattern = new RegExp(
`${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`,
'g'
)
const tagMatches = resolved.match(tagPattern) || []
for (const match of tagMatches) {
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
const replacements: Array<{ match: string; index: number; value: string }> = []
let match: RegExpExecArray | null
while ((match = tagPattern.exec(command)) !== null) {
const tagName = match[1].trim()
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
const blockName = pathParts[0]
const fieldPath = pathParts.slice(1)
@@ -131,7 +131,13 @@ function resolveTagVariables(
stringValue = String(result.value)
}
resolved = resolved.replace(new RegExp(escapeRegExp(match), 'g'), () => stringValue)
replacements.push({ match: match[0], index: match.index, value: stringValue })
}
let resolved = command
for (let i = replacements.length - 1; i >= 0; i--) {
const { match: matchStr, index, value } = replacements[i]
resolved = resolved.slice(0, index) + value + resolved.slice(index + matchStr.length)
}
return resolved
@@ -243,7 +249,6 @@ export async function POST(req: NextRequest) {
const {
command,
timeout = DEFAULT_EXECUTION_TIMEOUT_MS,
workingDirectory,
envVars = {},
blockData = {},
@@ -253,6 +258,9 @@ export async function POST(req: NextRequest) {
workflowId,
} = body
const parsedTimeout = Number(body.timeout)
const timeout = parsedTimeout > 0 ? parsedTimeout : DEFAULT_EXECUTION_TIMEOUT_MS
if (!command || typeof command !== 'string') {
return NextResponse.json(
{ success: false, error: 'Command is required and must be a string' },

View File

@@ -17,6 +17,7 @@ export const ExecuteCommandBlock: BlockConfig<ExecuteCommandOutput> = {
- Use {{ENV_VAR}} syntax to reference environment variables.
- The working directory defaults to the server process directory if not specified.
- A non-zero exit code is returned as data (exitCode > 0), not treated as a workflow error. Use a Condition block to branch on exitCode if needed.
- Variable values from other blocks are interpolated directly into the command string. Avoid passing untrusted user input as block references to prevent shell injection.
`,
docsLink: 'https://docs.sim.ai/blocks/execute-command',
category: 'blocks',

View File

@@ -67,7 +67,7 @@ export const executeCommandRunTool: ToolConfig<ExecuteCommandInput, ExecuteComma
},
request: {
url: '/api/execute-command/run',
url: '/api/tools/execute-command/run',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',