From 9431e7699e0ea84c7d695c11a6ec74dc0a8297a5 Mon Sep 17 00:00:00 2001 From: "helpcode.ai" <340500827+helpcodeai@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:48:48 +0200 Subject: [PATCH] Refuse SQLite database connectors on Cloud A SQLite connector opens a path on the server's file system. On a self-hosted instance that is the operator's own disk, but on the multi-tenant cloud every workspace shares the container, so a workspace could open or create files there. The engine now refuses SQLite when DEPLOYMENT_MODE=cloud (execute and testConnection); self-hosted is unchanged. No cloud connector uses SQLite (23 database connectors checked). --- .../engines/database.engine.ssrf.spec.ts | 11 +++++++++++ .../src/connectors/engines/database.engine.ts | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/connectors/engines/database.engine.ssrf.spec.ts b/packages/backend/src/connectors/engines/database.engine.ssrf.spec.ts index bfcb49685..d9ac53c0c 100644 --- a/packages/backend/src/connectors/engines/database.engine.ssrf.spec.ts +++ b/packages/backend/src/connectors/engines/database.engine.ssrf.spec.ts @@ -18,6 +18,7 @@ describe('DatabaseEngine — SSRF guard', () => { delete process.env.SSRF_GUARD; delete process.env.SSRF_ALLOW_PRIVATE; delete process.env.SSRF_ALLOWED_HOSTS; + delete process.env.DEPLOYMENT_MODE; }); const cfg = (baseUrl: string) => ({ baseUrl, authType: 'none' }); @@ -73,4 +74,14 @@ describe('DatabaseEngine — SSRF guard', () => { engine.execute(cfg('sqlite:///nonexistent-amcp-test.db'), query, params), ).rejects.not.toBeInstanceOf(SsrfBlockedError); }); + + it('refuses sqlite on the cloud, where the file system is shared by every workspace', async () => { + process.env.DEPLOYMENT_MODE = 'cloud'; + await expect( + engine.execute(cfg('sqlite:///app/backend/data.db'), query, params), + ).rejects.toBeInstanceOf(SsrfBlockedError); + await expect(engine.testConnection(cfg('sqlite:/tmp/x.db'))).rejects.toBeInstanceOf( + SsrfBlockedError, + ); + }); }); diff --git a/packages/backend/src/connectors/engines/database.engine.ts b/packages/backend/src/connectors/engines/database.engine.ts index 30d6a06c9..1cef1aab7 100644 --- a/packages/backend/src/connectors/engines/database.engine.ts +++ b/packages/backend/src/connectors/engines/database.engine.ts @@ -5,7 +5,7 @@ import { MongoClient } from 'mongodb'; import * as mysql from 'mysql2/promise'; import * as oracledb from 'oracledb'; import Database from 'better-sqlite3'; -import { assertSafeOutboundHost } from '../../common/ssrf.util'; +import { SsrfBlockedError, assertSafeOutboundHost } from '../../common/ssrf.util'; import { HanaConnectOptions, HanaDriverName, @@ -831,14 +831,24 @@ export class DatabaseEngine { /** * SSRF guard for a database connection string. SQLite is a local file (no - * network host) so it's skipped here. Every other engine resolves one or more + * network host): on a self-hosted instance the operator's own disk is fair + * game, but on the multi-tenant cloud it would let any workspace open or + * create files inside the shared container, so it is refused there. Every + * other engine resolves one or more * hosts that must pass the same outbound policy as REST/SOAP/GraphQL — so a * connector can't be pointed at cloud metadata, loopback, or internal-only * databases unless an operator allowlists them (SSRF_ALLOWED_HOSTS / * SSRF_ALLOW_PRIVATE). */ private async assertSafeDbHost(baseUrl: string): Promise { - if (this.isSqlite(baseUrl)) return; + if (this.isSqlite(baseUrl)) { + if (process.env.DEPLOYMENT_MODE === 'cloud') { + throw new SsrfBlockedError( + 'SQLite connectors read files on the server and are not available on AnythingMCP Cloud. Use a network database (PostgreSQL, MySQL, SQL Server, Oracle, SAP HANA or MongoDB).', + ); + } + return; + } const hosts = this.extractHosts(baseUrl); for (const host of hosts) { await assertSafeOutboundHost(host);