{"record":{"id":"1ebf251b0af49b7f","repo":"ruvnet/ruflo","slug":"ssrf-guard-invalid-url-rawurl-1ebf25","errorCode":null,"errorMessage":"SSRF guard: invalid URL — ${rawUrl}","messagePattern":"SSRF guard: invalid URL — (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/mcp-bridge/index.js","lineNumber":743,"sourceCode":"    };\n  } catch (err) {\n    if (err.name === \"AbortError\" || err.name === \"TimeoutError\") return { error: \"Search timed out\" };\n    return { error: err.message };\n  }\n}\n\n// =============================================================================\n// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)\n// =============================================================================\n\nconst PRIVATE_IP_RE = /^(?:10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.|127\\.|0\\.|::1|fc|fd)/i;\n\nfunction assertSafeUrl(rawUrl) {\n  let parsed;\n  try {\n    parsed = new URL(rawUrl);\n  } catch {\n    throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);\n  }\n  if (parsed.protocol !== \"https:\") {\n    throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);\n  }\n  const host = parsed.hostname;\n  if (PRIVATE_IP_RE.test(host) || host === \"localhost\" || host.endsWith(\".local\")) {\n    throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);\n  }\n}\n\n// =============================================================================\n// HELPER — Call a backend Cloud Function / API\n// =============================================================================\n\nasync function callCloudFunction(url, payload, timeoutMs = 25000) {\n  // Validate the URL before making any network request.\n  assertSafeUrl(url);\n  const controller = new AbortController();","sourceCodeStart":725,"sourceCodeEnd":761,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/ruvocal/mcp-bridge/index.js#L725-L761","documentation":"handleTrajectoryEnd finalizes a trajectory: it stamps endedAt and the caller's verdict, computes duration/step metrics, and triggers learning if requested. The lookup state.trajectories.get(input.trajectoryId) must find the trajectory in the current process's in-memory Map, otherwise 'Trajectory <id> not found' is thrown and nothing is finalized. Ending is the terminal call for a trajectory lifecycle that must have started with begin in the same process.","triggerScenarios":"Calling sona_trajectory_end with an ID from before a server restart; ending an ID that was never begun (e.g. fabricated or from documentation examples like 'traj_123'); double-ending after the Map was cleared; passing the sessionId field instead of trajectoryId.","commonSituations":"Agent pipelines that span deployments and try to close old trajectories in a new process; log-replay tooling replaying end calls without the matching begins; tests that only exercise the end handler.","solutions":["Only end trajectories you began in the current process; keep the begin response's trajectoryId and thread it through to end","If the server restarted mid-run, begin a new trajectory and replay the important steps, then end that one","Guard the call: skip end (and log) when your local records show the trajectory predates the current process","Make sure you pass trajectoryId, not sessionId — begin returns both and mixing them up is a common cause"],"exampleFix":"// before\nawait client.callTool('sona_trajectory_end', { trajectoryId: sessionId, verdict: 'success' }); // wrong id kind -> throws [1131]\n\n// after\nconst { trajectoryId, sessionId } = await client.callTool('sona_trajectory_begin', { sessionId });\n// ... steps ...\nawait client.callTool('sona_trajectory_end', { trajectoryId, verdict: 'success', triggerLearning: true });","handlingStrategy":"validation","validationCode":"function shouldEndTrajectory(trajId: string, begunIds: Set<string>): boolean {\n  return begunIds.has(trajId);\n}\n// call only when shouldEndTrajectory(id) is true; otherwise skip end (nothing to finalize)","typeGuard":null,"tryCatchPattern":"try {\n  await client.callTool('sona_trajectory_end', { trajectoryId, verdict });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Trajectory') && e.message.includes('not found')) {\n    return { completed: false, reason: 'trajectory predates current process; nothing to finalize' };\n  }\n  throw e;\n}","preventionTips":["Store both trajectoryId and sessionId from begin and destructure carefully — mixing them is the top cause","End trajectories in the same finally-block scope where you began them","Treat end-after-restart as a no-op, not an error, in long-running pipelines"],"tags":["mcp","sona","trajectory","not-found","lifecycle"],"backgroundTag":"trajectory-not-found","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}