Skip to content
Open
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
6 changes: 5 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ services:
dokploy-network:
aliases:
- evolution-redis
ports:
- "6379:6379"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ Critical Β· Unauthenticated Redis is network-exposed

When the Compose host is reachable from an untrusted network, the published Redis port accepts connections on host interfaces, and Redis starts without authentication, so remote clients can read, alter, or delete cache data.

Remove the public Redis port mapping or bind it to localhost, and configure authentication if Redis must be reachable remotely.

Also at docker-compose.yaml:70-71.

Prompt for AI agents
In `docker-compose.yaml` at line 48:

**Unauthenticated Redis is network-exposed**

When the Compose host is reachable from an untrusted network, the published Redis port accepts connections on host interfaces, and Redis starts without authentication, so remote clients can read, alter, or delete cache data.

Remove the public Redis port mapping or bind it to localhost, and configure authentication if Redis must be reachable remotely.

Also at `docker-compose.yaml:70-71`.

From AGENTS.md L316:

- **Secure defaults** for all configurations

expose:
- "6379"

Expand All @@ -60,11 +62,13 @@ services:
- -c
- listen_addresses=*
environment:
- POSTGRES_DB=${POSTGRES_DATABASE}
- POSTGRES_DB=evolution_api
- POSTGRES_USER=${POSTGRES_USERNAME}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Medium · PostgreSQL is reachable off-host

When the Compose host is reachable from an untrusted network and the configured database password is weak or compromised, the 5432:5432 mapping binds PostgreSQL on every host interface, allowing remote clients to send login attempts directly to the database and, with a weak or compromised password, access application data using the configured database role.

Bind the PostgreSQL port to localhost for local development or restrict it to a trusted private network instead of publishing it on all host interfaces.

Prompt for AI agents
In `docker-compose.yaml` at line 71:

**PostgreSQL is reachable off-host**

When the Compose host is reachable from an untrusted network and the configured database password is weak or compromised, the `5432:5432` mapping binds PostgreSQL on every host interface, allowing remote clients to send login attempts directly to the database and, with a weak or compromised password, access application data using the configured database role.

Bind the PostgreSQL port to localhost for local development or restrict it to a trusted private network instead of publishing it on all host interfaces.

networks:
- evolution-net
- dokploy-network
Expand Down
83 changes: 48 additions & 35 deletions src/cache/rediscache.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,62 @@ class Redis {
private client: RedisClientType = null;
private conf: CacheConfRedis;
private connected = false;
private connecting: Promise<RedisClientType> | null = null;

constructor() {
this.conf = configService.get<CacheConf>('CACHE')?.REDIS;
}

getConnection(): RedisClientType {
if (this.connected) {
async getConnection(): Promise<RedisClientType> {
if (this.connected && this.client) {
return this.client;
} else {
this.client = createClient({
url: this.conf.URI,
});

this.client.on('connect', () => {
this.logger.verbose('redis connecting');
});

this.client.on('ready', () => {
this.logger.verbose('redis ready');
this.connected = true;
});

this.client.on('error', () => {
this.logger.error('redis disconnected');
this.connected = false;
});

this.client.on('end', () => {
this.logger.verbose('redis connection ended');
this.connected = false;
});

try {
this.client.connect();
this.connected = true;
} catch (e) {
this.connected = false;
this.logger.error('redis connect exception caught: ' + e);
return null;
}
}

return this.client;
if (this.connecting) {
return this.connecting;
}

this.connecting = this._connect();
return this.connecting;
}

private async _connect(): Promise<RedisClientType> {
this.client = createClient({
url: this.conf.URI,
});

this.client.on('connect', () => {
this.logger.verbose('redis connecting');
});

this.client.on('ready', () => {
this.logger.verbose('redis ready');
this.connected = true;
this.connecting = null;
});

this.client.on('error', () => {
this.logger.error('redis disconnected');
this.connected = false;
this.connecting = null;
});

this.client.on('end', () => {
this.logger.verbose('redis connection ended');
this.connected = false;
this.connecting = null;
});

try {
await this.client.connect();
} catch (e) {
this.connected = false;
this.connecting = null;
this.logger.error('redis connect exception caught: ' + e);
return null;
}

return this.client;
}
}

Expand Down
54 changes: 43 additions & 11 deletions src/cache/rediscache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,45 @@ import { redisClient } from './rediscache.client';

export class RedisCache implements ICache {
private readonly logger = new Logger('RedisCache');
private client: RedisClientType;
private conf: CacheConfRedis;
private client: RedisClientType = null;
private connecting: Promise<RedisClientType> | null = null;

constructor(
private readonly configService: ConfigService,
private readonly module: string,
) {
this.conf = this.configService.get<CacheConf>('CACHE')?.REDIS;
this.client = redisClient.getConnection();
}

private async ensureClient(): Promise<RedisClientType> {
if (this.client) {
return this.client;
}
if (this.connecting) {
return this.connecting;
}
this.connecting = redisClient.getConnection();
this.client = await this.connecting;
this.connecting = null;
return this.client;
}

async get(key: string): Promise<any> {
try {
return JSON.parse(await this.client.get(this.buildKey(key)));
const client = await this.ensureClient();
if (!client) return null;
return JSON.parse(await client.get(this.buildKey(key)));
} catch (error) {
this.logger.error(error);
}
}

async hGet(key: string, field: string) {
try {
const data = await this.client.hGet(this.buildKey(key), field);
const client = await this.ensureClient();
if (!client) return null;
const data = await client.hGet(this.buildKey(key), field);

if (data) {
return JSON.parse(data, BufferJSON.reviver);
Expand All @@ -42,64 +60,78 @@ export class RedisCache implements ICache {

async set(key: string, value: any, ttl?: number) {
try {
await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value));
const client = await this.ensureClient();
if (!client) return;
await client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value));
} catch (error) {
this.logger.error(error);
}
}

async hSet(key: string, field: string, value: any) {
try {
const client = await this.ensureClient();
if (!client) return;
const json = JSON.stringify(value, BufferJSON.replacer);

await this.client.hSet(this.buildKey(key), field, json);
await client.hSet(this.buildKey(key), field, json);
} catch (error) {
this.logger.error(error);
}
}

async has(key: string) {
try {
return (await this.client.exists(this.buildKey(key))) > 0;
const client = await this.ensureClient();
if (!client) return false;
return (await client.exists(this.buildKey(key))) > 0;
} catch (error) {
this.logger.error(error);
}
}

async delete(key: string) {
try {
return await this.client.del(this.buildKey(key));
const client = await this.ensureClient();
if (!client) return 0;
return await client.del(this.buildKey(key));
} catch (error) {
this.logger.error(error);
}
}

async hDelete(key: string, field: string) {
try {
return await this.client.hDel(this.buildKey(key), field);
const client = await this.ensureClient();
if (!client) return false;
return await client.hDel(this.buildKey(key), field);
} catch (error) {
this.logger.error(error);
}
}

async deleteAll(appendCriteria?: string) {
try {
const client = await this.ensureClient();
if (!client) return 0;
const keys = await this.keys(appendCriteria);
if (!keys?.length) {
return 0;
}

return await this.client.del(keys);
return await client.del(keys);
} catch (error) {
this.logger.error(error);
}
}

async keys(appendCriteria?: string) {
try {
const client = await this.ensureClient();
if (!client) return [];
const match = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}*`;
const keys = [];
for await (const key of this.client.scanIterator({
for await (const key of client.scanIterator({
MATCH: match,
COUNT: 100,
})) {
Expand Down