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 bfcb4968..d9ac53c0 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 30d6a06c..1cef1aab 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);