From 22735131be25c4e985708d4337e3a236c29341e1 Mon Sep 17 00:00:00 2001 From: kodinaka30-ship-it Date: Tue, 28 Jul 2026 13:58:16 +0100 Subject: [PATCH] feat(security): add security header sweep for /api/indexer (#634) Set and verify CSP, X-Content-Type-Options, and Referrer-Policy on /api/indexer responses. Closes #634 --- src/index.ts | 4 ++-- src/middleware/securityHeaders.ts | 3 ++- src/routes/indexer.ts | 22 ++++++++++++++++++++++ src/routes/indexer/health.ts | 3 +++ tests/indexerHealth.test.ts | 21 +++++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/routes/indexer.ts diff --git a/src/index.ts b/src/index.ts index b8a32d3..728ccb3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ import { register } from "./metrics/registry"; import { connectWithRetry, closeDb, db } from "./db/client"; import { stopScheduler } from "./services/scheduler"; import { startIndexerHealthProbe } from "./jobs/indexerHealthProbe"; -import { indexerHealthRouter } from "./routes/indexer/health"; +import { indexerRouter } from "./routes/indexer"; import { WebhookWorker } from "./workers/webhookWorker"; import { marketResolverWorker } from "./workers/marketResolver"; import { backupVerificationWorker } from "./workers/backupVerificationWorker"; @@ -150,7 +150,7 @@ export function createApp(_options: CreateAppOptions = {}): express.Express { app.use("/api/health/ready", createReadyRouter({ db, redis: redisConnection })); app.use("/api/health/dependencies", dependenciesRouter); app.use("/api/health/version", versionRouter); - app.use("/api/indexer", indexerHealthRouter); + app.use("/api/indexer", indexerRouter); const mutationMethods = ["POST", "PATCH"] as const; app.use("/api", (req, res, next) => diff --git a/src/middleware/securityHeaders.ts b/src/middleware/securityHeaders.ts index a4b93fa..90ad65f 100644 --- a/src/middleware/securityHeaders.ts +++ b/src/middleware/securityHeaders.ts @@ -29,7 +29,8 @@ * import { securityHeaders } from "../middleware/securityHeaders"; * * // Mount first, before auth/business-logic middleware, so the headers - * // are present on every response from this router — including 401/403s. + * // are present on every response from this router — including 401/403s + * // and indexer responses (/api/indexer). * someRouter.use(securityHeaders); */ diff --git a/src/routes/indexer.ts b/src/routes/indexer.ts new file mode 100644 index 0000000..81762ae --- /dev/null +++ b/src/routes/indexer.ts @@ -0,0 +1,22 @@ +/** + * @module routes/indexer + * + * Router for `/api/indexer` endpoints, ensuring standard API security headers + * (`Content-Security-Policy`, `X-Content-Type-Options`, `Referrer-Policy`) + * are set on all responses. + */ + +import { Router } from "express"; +import { securityHeaders } from "../middleware/securityHeaders"; +import { + createIndexerHealthRouter, + indexerHealthRouter, +} from "./indexer/health"; + +export const indexerRouter = Router(); + +// Apply security response headers to all /api/indexer routes +indexerRouter.use(securityHeaders); +indexerRouter.use(indexerHealthRouter); + +export { createIndexerHealthRouter, indexerHealthRouter }; diff --git a/src/routes/indexer/health.ts b/src/routes/indexer/health.ts index 711f2de..4be2dd7 100644 --- a/src/routes/indexer/health.ts +++ b/src/routes/indexer/health.ts @@ -65,6 +65,7 @@ import { env } from "../../config/env"; import { logger } from "../../config/logger"; import { getRequestId } from "../../lib/requestContext"; import { conditionalGet } from "../../middleware/etag"; +import { securityHeaders } from "../../middleware/securityHeaders"; import { indexerService } from "../../services/indexerService"; // ─── Types ─────────────────────────────────────────────────────────────── @@ -183,6 +184,8 @@ export function createIndexerHealthRouter(deps: IndexerHealthRouterDeps = {}): R const probeRpc: ProbeSorobanRpcFn = deps.probeSorobanRpc ?? defaultProbeSorobanRpc; const router = Router(); + router.use(securityHeaders); + router.get("/health", async (req, res, next) => { const reqId = getRequestId(); const correlationId = diff --git a/tests/indexerHealth.test.ts b/tests/indexerHealth.test.ts index 4adb146..0f17625 100644 --- a/tests/indexerHealth.test.ts +++ b/tests/indexerHealth.test.ts @@ -23,6 +23,7 @@ import express from "express"; import { createIndexerHealthRouter } from "../src/routes/indexer/health"; import { indexerService } from "../src/services/indexerService"; import { errorHandler } from "../src/middleware/errorHandler"; +import { API_SECURITY_HEADERS } from "../src/middleware/securityHeaders"; // ── Types ──────────────────────────────────────────────────────────────────── @@ -351,6 +352,26 @@ describe("GET /api/indexer/health — other behaviours", () => { expect(res.status).toBe(200); }); + it("sets CSP, X-Content-Type-Options, and Referrer-Policy response headers on /api/indexer responses", async () => { + const res = await request( + makeApp({ + probeDatabase: () => Promise.resolve(ALL_OK.database), + probeSorobanRpc: () => Promise.resolve(ALL_OK.sorobanRpc), + }), + ).get(URL); + + expect(res.status).toBe(200); + expect(res.headers["content-security-policy"]).toBe( + API_SECURITY_HEADERS["Content-Security-Policy"], + ); + expect(res.headers["x-content-type-options"]).toBe( + API_SECURITY_HEADERS["X-Content-Type-Options"], + ); + expect(res.headers["referrer-policy"]).toBe( + API_SECURITY_HEADERS["Referrer-Policy"], + ); + }); + it("echoes x-correlation-id header when provided", async () => { const id = "indexer-health-trace-123"; const res = await request(