{"record":{"id":"b9e91fe32955b78e","repo":"ruvnet/ruflo","slug":"ssrf-guard-private-loopback-host-rejected-hos","errorCode":null,"errorMessage":"SSRF guard: private/loopback host rejected — ${host}","messagePattern":"SSRF guard: private/loopback host rejected — (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/mcp-bridge/index.js","lineNumber":661,"sourceCode":"// =============================================================================\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();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  try {\n    const resp = await fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(payload),\n      signal: controller.signal,","sourceCodeStart":643,"sourceCodeEnd":679,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/mcp-bridge/index.js#L643-L679","documentation":"handleTrajectoryContext merges input.context into an existing trajectory found via state.trajectories.get(input.trajectoryId). Like the other trajectory handlers it only knows the in-memory Map filled by sona_trajectory_begin, so an unknown ID — never begun, typo'd, or from a dead process — throws 'Trajectory <id> not found'. The update is a shallow merge of context keys and returns the resulting key list on success.","triggerScenarios":"sona_trajectory_context with an ID string that differs by one character from the begun ID; calling context-update after a server restart wiped the Map; using a stepId (prefixed 'step_') instead of a trajectoryId (prefixed 'traj_') from a earlier response.","commonSituations":" Mixing up IDs from different parts of the SONA API (traj_ vs step_ vs session_ prefixes); resuming a workflow after a crash without re-beginning; concurrent test suites sharing a server and clearing state.","solutions":["Use the exact trajectoryId returned by sona_trajectory_begin (IDs look like traj_<base36 time>_<random>)","After any MCP server restart, begin a new trajectory before issuing context updates","Store begun trajectoryIds in your orchestration state and validate against them before calling context","Distinguish ID prefixes: only traj_* IDs are valid here, not step_* or session_*"],"exampleFix":"// before\nawait client.callTool('sona_trajectory_context', { trajectoryId: stepId, context: { env: 'prod' } }); // stepId is step_... -> throws [1130]\n\n// after\nawait client.callTool('sona_trajectory_context', { trajectoryId, context: { env: 'prod' } }); // the traj_... id from begin","handlingStrategy":"validation","validationCode":"function canUpdateTrajectoryContext(trajId: string, begunIds: Set<string>): boolean {\n  return begunIds.has(trajId) && /^traj_/.test(trajId);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.callTool('sona_trajectory_context', { trajectoryId, context });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Trajectory') && e.message.includes('not found')) {\n    // trajectory Map was reset (restart) — begin a new one and re-apply context\n    const { trajectoryId: fresh } = await client.callTool('sona_trajectory_begin', {});\n    return client.callTool('sona_trajectory_context', { trajectoryId: fresh, context });\n  }\n  throw e;\n}","preventionTips":["Pass the traj_ id, never a step_ or session_ id, to context updates","Batch context updates right after begin so the lifecycle stays short-lived and restart-safe","Maintain your own begun-ID registry and drop it whenever you detect a server restart"],"tags":["mcp","sona","trajectory","not-found","in-memory-state"],"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"}