Skip to content

Commit cb6b81d

Browse files
ersinkocclaude
andcommitted
fix(security): disable OpenAPI docs in production by default (API-001)
GET /openapi.json and /docs exposed the entire API surface to anonymous callers. They are a development aid, so they are now disabled when NODE_ENV=production unless the operator opts in with ENABLE_API_DOCS=true. Non-production behaviour is unchanged. A hard on/off switch is used rather than session-auth gating to avoid the IDOR/login-loop-prone auth path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 116cf67 commit cb6b81d

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

packages/gateway/src/routes/openapi.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* fake API endpoints and verifies the served spec covers them.
66
*/
77

8-
import { describe, it, expect, beforeEach } from 'vitest';
8+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
99
import { Hono } from 'hono';
1010
import { registerOpenApiRoutes } from './openapi.js';
1111

@@ -60,4 +60,32 @@ describe('OpenAPI routes', () => {
6060
expect(html).toContain('swagger-ui');
6161
expect(html).toContain('/openapi.json');
6262
});
63+
64+
describe('API-001 production gate', () => {
65+
const prevEnv = process.env.NODE_ENV;
66+
const prevFlag = process.env.ENABLE_API_DOCS;
67+
afterEach(() => {
68+
process.env.NODE_ENV = prevEnv;
69+
if (prevFlag === undefined) delete process.env.ENABLE_API_DOCS;
70+
else process.env.ENABLE_API_DOCS = prevFlag;
71+
});
72+
73+
it('does not register docs in production by default', async () => {
74+
process.env.NODE_ENV = 'production';
75+
delete process.env.ENABLE_API_DOCS;
76+
const prod = new Hono();
77+
registerOpenApiRoutes(prod);
78+
expect((await prod.request('/openapi.json')).status).toBe(404);
79+
expect((await prod.request('/docs')).status).toBe(404);
80+
});
81+
82+
it('registers docs in production when ENABLE_API_DOCS=true', async () => {
83+
process.env.NODE_ENV = 'production';
84+
process.env.ENABLE_API_DOCS = 'true';
85+
const prod = new Hono();
86+
registerOpenApiRoutes(prod);
87+
expect((await prod.request('/openapi.json')).status).toBe(200);
88+
expect((await prod.request('/docs')).status).toBe(200);
89+
});
90+
});
6391
});

packages/gateway/src/routes/openapi.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ import { SWAGGER_UI_HTML } from '../openapi/swagger-html.js';
1515
import { VERSION } from '@ownpilot/core';
1616

1717
export function registerOpenApiRoutes(app: Hono): void {
18+
// API-001: the OpenAPI spec and Swagger UI disclose the entire API surface to
19+
// anonymous callers. They are a development aid, so they are disabled in
20+
// production unless an operator explicitly opts in via ENABLE_API_DOCS=true.
21+
// Outside production they remain on for convenience. This deliberately avoids
22+
// wiring docs into the session-auth model (which has had IDOR/login-loop
23+
// regressions) — a hard on/off switch is the lower-risk control.
24+
const docsEnabled =
25+
process.env.NODE_ENV !== 'production' || process.env.ENABLE_API_DOCS === 'true';
26+
if (!docsEnabled) return;
27+
1828
let cached: OpenApiSpec | null = null;
1929

2030
app.get('/openapi.json', (c) => {

0 commit comments

Comments
 (0)