ruvnet/ruflo · error · Error
SSRF guard: private/loopback host rejected
Error message
SSRF guard: private/loopback host rejected — ${host} What it means
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.
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_*
Example fix
// before
await client.callTool('sona_trajectory_context', { trajectoryId: stepId, context: { env: 'prod' } }); // stepId is step_... -> throws [1130]
// after
await client.callTool('sona_trajectory_context', { trajectoryId, context: { env: 'prod' } }); // the traj_... id from begin Defensive patterns
Strategy: validation
Validate before calling
function canUpdateTrajectoryContext(trajId: string, begunIds: Set<string>): boolean {
return begunIds.has(trajId) && /^traj_/.test(trajId);
} Try / catch
try {
await client.callTool('sona_trajectory_context', { trajectoryId, context });
} catch (e) {
if (e instanceof Error && e.message.includes('Trajectory') && e.message.includes('not found')) {
// trajectory Map was reset (restart) — begin a new one and re-apply context
const { trajectoryId: fresh } = await client.callTool('sona_trajectory_begin', {});
return client.callTool('sona_trajectory_context', { trajectoryId: fresh, context });
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- SSRF guard: only HTTPS URLs are permitted, got
- SSRF guard: invalid URL
- SSRF guard: only HTTPS URLs are permitted, got
- SSRF guard: private/loopback host rejected
- User not found
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/b9e91fe32955b78e.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/mcp-bridge/index.js:661
// =============================================================================
// SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
// =============================================================================
const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
function assertSafeUrl(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
}
if (parsed.protocol !== "https:") {
throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
}
const host = parsed.hostname;
if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
}
}
// =============================================================================
// HELPER — Call a backend Cloud Function / API
// =============================================================================
async function callCloudFunction(url, payload, timeoutMs = 25000) {
// Validate the URL before making any network request.
assertSafeUrl(url);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal,View on GitHub (pinned to fa13ee4ad6)