From acd7be34e2de1fec8d0488203eb8e3999de1ff9e Mon Sep 17 00:00:00 2001 From: HusseinAdeiza Date: Mon, 28 Sep 2026 22:13:07 +0200 Subject: [PATCH] fix(security): bind the local Postgres port to loopback only docker-compose.yml published Postgres as ${DBPORT:-5432}:5432 with no host IP, so Docker bound 0.0.0.0 and the container was reachable from any interface. .env.example ships DBUSER=postgres / DBPASSWORD=postgres as the defaults, so a stock `genlayer init` on a host with a public IP exposed a superuser Postgres on 5432, which is enough for RCE via CREATE FUNCTION plus libc.so.6. The localnet only ever reaches Postgres over the compose network as postgres:5432, so the published port is not needed for the stack to work. Binding it to 127.0.0.1 keeps psql and other host-side tooling working while removing the public exposure. Fixes #1603. --- docker-compose.yml | 2 +- tests/docker-compose-ports.test.ts | 36 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/docker-compose-ports.test.ts diff --git a/docker-compose.yml b/docker-compose.yml index 6e8c4a0b..99419b4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -103,7 +103,7 @@ services: postgres: image: postgres:17-alpine ports: - - "${DBPORT:-5432}:5432" + - "127.0.0.1:${DBPORT:-5432}:5432" environment: - POSTGRES_USER=${DBUSER:-postgres} - POSTGRES_PASSWORD=${DBPASSWORD:-postgres} diff --git a/tests/docker-compose-ports.test.ts b/tests/docker-compose-ports.test.ts new file mode 100644 index 00000000..7b0d3413 --- /dev/null +++ b/tests/docker-compose-ports.test.ts @@ -0,0 +1,36 @@ +import {readFileSync} from "node:fs"; +import {dirname, resolve} from "node:path"; +import {fileURLToPath} from "node:url"; +import {describe, expect, it} from "vitest"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +const composeFile = readFileSync(resolve(repositoryRoot, "docker-compose.yml"), "utf-8"); + +function publishedPortsFor(service: string): string[] { + const serviceBlock = composeFile.match(new RegExp(`^ ${service}:\\n([\\s\\S]*?)(?=^ \\S|\\Z)`, "m")); + + if (!serviceBlock) { + throw new Error(`service "${service}" not found in docker-compose.yml`); + } + + const portsBlock = serviceBlock[1].match(/^\s{4}ports:\n([\s\S]*?)(?=^\s{4}\S|\Z)/m); + + if (!portsBlock) { + return []; + } + + return [...portsBlock[1].matchAll(/^\s+- "([^"]+)"$/gm)].map(match => match[1]); +} + +describe("docker-compose port bindings", () => { + it("publishes postgres on loopback only", () => { + expect(publishedPortsFor("postgres")).toEqual(["127.0.0.1:${DBPORT:-5432}:5432"]); + }); + + it("binds every published postgres port to an explicit host address", () => { + for (const port of publishedPortsFor("postgres")) { + expect(port).toMatch(/^127\.0\.0\.1:/); + } + }); +});