Summary
Rowboat lets a project member configure two kinds of outbound URLs that the server then connects to on the project's behalf: a custom MCP server URL (used at agent runtime to list and invoke MCP tools) and a project webhook URL (used by the built-in webhook tool). Neither is validated against private/loopback/link-local ranges; the only check on the MCP URL is that the scheme is http/https. Because any registered user can create a project and is a member of it, any such user can point these URLs at internal services or the cloud metadata endpoint and cause the Rowboat server to connect to them, turning the shared server into an SSRF probe of its own internal network. The webhook tool surfaces the upstream HTTP status/statusText in its error, and the MCP connection succeeds or fails depending on reachability, giving a reliable internal-service/port oracle.
Details
The custom MCP server URL is only protocol-checked (apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts):
function validateHttpHttpsUrl(url: string): string {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
return parsedUrl.toString(); // no private-IP / metadata / DNS-rebinding check
}
At agent runtime the stored URL is connected to from the server (apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts):
import { getMcpClient } from "@/app/lib/mcp";
...
const client = await getMcpClient(mcpServerURL, mcpServerName); // StreamableHTTPClientTransport connects to mcpServerURL
The webhook tool fetches project.webhookUrl with no SSRF guard and reflects the upstream status (apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts):
if (!project.webhookUrl) {
throw new Error('Webhook URL not found');
}
...
const response = await fetch(project.webhookUrl, { ... });
if (!response.ok) {
throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`); // status/statusText oracle
}
Both mcpServerURL and webhookUrl are project configuration set by a project member through the authenticated API; the project-action-authorization policy correctly scopes these to the project, but it does not constrain the URL's destination. There is no allowlist and no resolution-time private-IP block, so values such as http://169.254.169.254/..., http://127.0.0.1:6379/, or http://10.x.x.x:port/ are accepted and connected to from the server. (Project members reading their own project's API keys/secret is by-design and is not part of this report; the issue here is the missing destination validation on outbound URLs.)
POC available upon request.
Impact
Any registered user (via a project they own) can make the Rowboat server issue requests to arbitrary internal hosts and the cloud metadata service, with no private-IP restriction. Even with the limited readback (HTTP status/statusText from the webhook tool, and connection success/failure from the MCP client), this yields internal port/service discovery and metadata-endpoint reachability from a low-privilege, multi-tenant boundary; depending on the deployment's internal services it can escalate to data access. Fix: validate and resolve the host of every user-configured outbound URL (MCP server URL, webhook URL) and reject loopback, link-local (169.254.0.0/16), and RFC1918/ULA ranges, re-checking after DNS resolution and on redirects; restrict schemes to http(s); and avoid reflecting upstream response details for these tools.
Summary
Rowboat lets a project member configure two kinds of outbound URLs that the server then connects to on the project's behalf: a custom MCP server URL (used at agent runtime to list and invoke MCP tools) and a project webhook URL (used by the built-in webhook tool). Neither is validated against private/loopback/link-local ranges; the only check on the MCP URL is that the scheme is
http/https. Because any registered user can create a project and is a member of it, any such user can point these URLs at internal services or the cloud metadata endpoint and cause the Rowboat server to connect to them, turning the shared server into an SSRF probe of its own internal network. The webhook tool surfaces the upstream HTTP status/statusTextin its error, and the MCP connection succeeds or fails depending on reachability, giving a reliable internal-service/port oracle.Details
The custom MCP server URL is only protocol-checked (
apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts):At agent runtime the stored URL is connected to from the server (
apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts):The webhook tool fetches
project.webhookUrlwith no SSRF guard and reflects the upstream status (apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts):Both
mcpServerURLandwebhookUrlare project configuration set by a project member through the authenticated API; the project-action-authorization policy correctly scopes these to the project, but it does not constrain the URL's destination. There is no allowlist and no resolution-time private-IP block, so values such ashttp://169.254.169.254/...,http://127.0.0.1:6379/, orhttp://10.x.x.x:port/are accepted and connected to from the server. (Project members reading their own project's API keys/secret is by-design and is not part of this report; the issue here is the missing destination validation on outbound URLs.)POC available upon request.
Impact
Any registered user (via a project they own) can make the Rowboat server issue requests to arbitrary internal hosts and the cloud metadata service, with no private-IP restriction. Even with the limited readback (HTTP status/
statusTextfrom the webhook tool, and connection success/failure from the MCP client), this yields internal port/service discovery and metadata-endpoint reachability from a low-privilege, multi-tenant boundary; depending on the deployment's internal services it can escalate to data access. Fix: validate and resolve the host of every user-configured outbound URL (MCP server URL, webhook URL) and reject loopback, link-local (169.254.0.0/16), and RFC1918/ULA ranges, re-checking after DNS resolution and on redirects; restrict schemes to http(s); and avoid reflecting upstream response details for these tools.