From c50ea76477b337f1a5297f9f2d3067bc71dd066b Mon Sep 17 00:00:00 2001 From: felixvippp-ai Date: Tue, 23 Jun 2026 23:26:10 -0400 Subject: [PATCH] security: configure server timeouts --- README.md | 12 ++++++ src/index.ts | 2 + src/server-timeouts.test.ts | 86 +++++++++++++++++++++++++++++++++++++ src/serverTimeouts.ts | 50 +++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 src/server-timeouts.test.ts create mode 100644 src/serverTimeouts.ts diff --git a/README.md b/README.md index fa721c7..4921d33 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,18 @@ agentpay-backend/ | `npm run dev` | Run with ts-node | | `npm start` | Run production build | +## Server timeouts / DoS hardening + +The HTTP server configures bounded timeouts after startup: + +- `REQUEST_TIMEOUT_MS` defaults to `120000`. +- `HEADERS_TIMEOUT_MS` defaults to `65000`. +- `KEEPALIVE_TIMEOUT_MS` defaults to `5000`. + +Invalid, zero, or negative timeout overrides fall back to the defaults. +`HEADERS_TIMEOUT_MS` is raised when needed so it is always at least +`KEEPALIVE_TIMEOUT_MS`. + ## CI/CD On push/PR to `main`, GitHub Actions runs: diff --git a/src/index.ts b/src/index.ts index 9ece0f2..20f62e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { createMetricsRouter } from "./routes/metrics.js"; import { createServicesRouter } from "./routes/services.js"; import { createUsageRouter } from "./routes/usage.js"; import { createWebhooksRouter } from "./routes/webhooks.js"; +import { configureServerTimeouts } from "./serverTimeouts.js"; const PORT = process.env.PORT ?? 3001; @@ -48,6 +49,7 @@ if (process.argv[1]?.endsWith("index.js") || process.argv[1]?.endsWith("index.ts const server = app.listen(PORT, () => { console.log(`AgentPay backend listening on port ${PORT}`); }); + configureServerTimeouts(server); const shutdown = (signal: string) => { console.log(`Received ${signal}, draining…`); diff --git a/src/server-timeouts.test.ts b/src/server-timeouts.test.ts new file mode 100644 index 0000000..0184210 --- /dev/null +++ b/src/server-timeouts.test.ts @@ -0,0 +1,86 @@ +import { createServer } from "node:http"; +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { configureServerTimeouts } from "./serverTimeouts.js"; + +void describe("server timeout configuration", () => { + void it("applies safe defaults when env overrides are absent", () => { + const server = createServer(); + try { + const config = configureServerTimeouts(server, {}); + + assert.deepStrictEqual(config, { + requestTimeoutMs: 120_000, + headersTimeoutMs: 65_000, + keepAliveTimeoutMs: 5_000, + }); + assert.strictEqual(server.requestTimeout, 120_000); + assert.strictEqual(server.headersTimeout, 65_000); + assert.strictEqual(server.keepAliveTimeout, 5_000); + assert.strictEqual(server.timeout, 120_000); + } finally { + server.close(); + } + }); + + void it("uses positive integer env overrides", () => { + const server = createServer(); + try { + const config = configureServerTimeouts(server, { + REQUEST_TIMEOUT_MS: "45000", + HEADERS_TIMEOUT_MS: "15000", + KEEPALIVE_TIMEOUT_MS: "10000", + }); + + assert.deepStrictEqual(config, { + requestTimeoutMs: 45_000, + headersTimeoutMs: 15_000, + keepAliveTimeoutMs: 10_000, + }); + assert.strictEqual(server.requestTimeout, 45_000); + assert.strictEqual(server.headersTimeout, 15_000); + assert.strictEqual(server.keepAliveTimeout, 10_000); + assert.strictEqual(server.timeout, 45_000); + } finally { + server.close(); + } + }); + + void it("falls back on invalid env values", () => { + const server = createServer(); + try { + const config = configureServerTimeouts(server, { + REQUEST_TIMEOUT_MS: "not-a-number", + HEADERS_TIMEOUT_MS: "0", + KEEPALIVE_TIMEOUT_MS: "-1", + }); + + assert.deepStrictEqual(config, { + requestTimeoutMs: 120_000, + headersTimeoutMs: 65_000, + keepAliveTimeoutMs: 5_000, + }); + } finally { + server.close(); + } + }); + + void it("keeps headersTimeout greater than or equal to keepAliveTimeout", () => { + const server = createServer(); + try { + const config = configureServerTimeouts(server, { + REQUEST_TIMEOUT_MS: "30000", + HEADERS_TIMEOUT_MS: "5000", + KEEPALIVE_TIMEOUT_MS: "20000", + }); + + assert.strictEqual(config.headersTimeoutMs, 20_000); + assert.strictEqual(config.keepAliveTimeoutMs, 20_000); + assert.ok(config.headersTimeoutMs >= config.keepAliveTimeoutMs); + assert.strictEqual(server.headersTimeout, 20_000); + assert.strictEqual(server.keepAliveTimeout, 20_000); + } finally { + server.close(); + } + }); +}); diff --git a/src/serverTimeouts.ts b/src/serverTimeouts.ts new file mode 100644 index 0000000..32773d3 --- /dev/null +++ b/src/serverTimeouts.ts @@ -0,0 +1,50 @@ +import type { Server } from "node:http"; + +export type ServerTimeoutConfig = { + requestTimeoutMs: number; + headersTimeoutMs: number; + keepAliveTimeoutMs: number; +}; + +const DEFAULT_REQUEST_TIMEOUT_MS = 120_000; +const DEFAULT_HEADERS_TIMEOUT_MS = 65_000; +const DEFAULT_KEEPALIVE_TIMEOUT_MS = 5_000; + +function readPositiveInt(value: string | undefined, fallback: number): number { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.trunc(parsed); +} + +/** + * Applies bounded HTTP server timeouts so slow or incomplete clients cannot + * hold sockets indefinitely. Headers timeout is raised when needed to preserve + * Node's headersTimeout >= keepAliveTimeout invariant. + */ +export function configureServerTimeouts( + server: Server, + env: NodeJS.ProcessEnv = process.env +): ServerTimeoutConfig { + const requestTimeoutMs = readPositiveInt( + env.REQUEST_TIMEOUT_MS, + DEFAULT_REQUEST_TIMEOUT_MS + ); + const keepAliveTimeoutMs = readPositiveInt( + env.KEEPALIVE_TIMEOUT_MS, + DEFAULT_KEEPALIVE_TIMEOUT_MS + ); + const requestedHeadersTimeoutMs = readPositiveInt( + env.HEADERS_TIMEOUT_MS, + DEFAULT_HEADERS_TIMEOUT_MS + ); + const headersTimeoutMs = Math.max(requestedHeadersTimeoutMs, keepAliveTimeoutMs); + + server.requestTimeout = requestTimeoutMs; + server.headersTimeout = headersTimeoutMs; + server.keepAliveTimeout = keepAliveTimeoutMs; + server.setTimeout(requestTimeoutMs, (socket) => { + socket.destroy(); + }); + + return { requestTimeoutMs, headersTimeoutMs, keepAliveTimeoutMs }; +}