Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down Expand Up @@ -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,
);
});
});
16 changes: 13 additions & 3 deletions packages/backend/src/connectors/engines/database.engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<void> {
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);
Expand Down
Loading