API documentation is essential for developers but can become a security liability if it exposes sensitive information. This guide covers securing OpenAPI/Swagger specifications and documentation portals.
Documentation Security Risks
┌─────────────────────────────────────────────────────────────────────────────┐
│ INFORMATION DISCLOSED IN API DOCS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ PUBLIC DOCS LEAKING... ATTACKER USES FOR... │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ • Admin endpoint paths → Direct exploitation │
│ • Internal endpoint patterns → Endpoint enumeration │
│ • Authentication mechanisms → Auth bypass research │
│ • Parameter validation rules → Boundary testing │
│ • Rate limit thresholds → DoS planning │
│ • Error message formats → Error-based attacks │
│ • Data model structures → SQL/NoSQL injection │
│ • Real example data (PII) → Social engineering │
│ • Version numbers → Known CVE exploitation │
│ • Internal hostnames → Network mapping │
│ │
│ DOCUMENTATION IS RECONNAISSANCE │
│ Attackers read your docs before attacking your API │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tiered Documentation Strategy
Separate Specs for Different Audiences
# public-openapi.yaml - External developers
openapi: 3.1.0
info:
title: MyApp Public API
version: 1.0.0
paths:
/users:
get:
summary: List users
# Public endpoints only
/products:
get:
summary: List products
# No admin endpoints, no internal routes
# internal-openapi.yaml - Internal developers only
openapi: 3.1.0
info:
title: MyApp Internal API
version: 1.0.0
paths:
# Public endpoints
/users:
get:
summary: List users
# Internal endpoints marked
/admin/users:
x-internal: true
get:
summary: Admin user management
/internal/metrics:
x-internal: true
get:
summary: System metrics
/debug/cache:
x-internal: true
delete:
summary: Clear cache
Build-Time Filtering
// scripts/filter-openapi.ts
import { OpenAPIV3 } from 'openapi-types';
import yaml from 'js-yaml';
import fs from 'fs';
function filterInternalEndpoints(spec: OpenAPIV3.Document): OpenAPIV3.Document {
const filtered = { ...spec, paths: {} };
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
// Skip paths marked as internal
if ((pathItem as any)['x-internal']) {
continue;
}
// Skip individual operations marked as internal
const filteredPathItem: OpenAPIV3.PathItemObject = {};
for (const [method, operation] of Object.entries(pathItem || {})) {
if (method.startsWith('x-')) continue;
if ((operation as any)['x-internal']) continue;
filteredPathItem[method as keyof OpenAPIV3.PathItemObject] = operation;
}
if (Object.keys(filteredPathItem).length > 0) {
filtered.paths[path] = filteredPathItem;
}
}
// Also filter schemas marked as internal
if (filtered.components?.schemas) {
for (const [name, schema] of Object.entries(filtered.components.schemas)) {
if ((schema as any)['x-internal']) {
delete filtered.components.schemas[name];
}
}
}
return filtered;
}
// Generate public spec
const internalSpec = yaml.load(
fs.readFileSync('./openapi-internal.yaml', 'utf8')
) as OpenAPIV3.Document;
const publicSpec = filterInternalEndpoints(internalSpec);
fs.writeFileSync(
'./public/openapi.yaml',
yaml.dump(publicSpec)
);
console.log('Generated filtered public OpenAPI spec');
Sanitizing Examples
Fake Data Guidelines
# ❌ BAD - Real or realistic data
examples:
user:
value:
id: 12847293
email: [email protected]
phone: +1-555-123-4567
ssn: "123-45-6789"
address: "123 Main St, San Francisco, CA 94102"
api_key: "sk_live_51ABC123DEF456..."
# ✅ GOOD - Obviously fake data
examples:
user:
value:
id: 123
email: [email protected]
phone: "+1-555-000-0000"
ssn: "000-00-0000"
address: "742 Evergreen Terrace, Springfield"
api_key: "your-api-key-here"
Automated Example Generation
import { faker } from '@faker-js/faker';
// Generate safe fake data for examples
function generateSafeExample(schema: any): any {
if (schema.type === 'string') {
if (schema.format === 'email') {
return '[email protected]';
}
if (schema.format === 'uri') {
return 'https://example.com/path';
}
if (schema.format === 'uuid') {
return '00000000-0000-0000-0000-000000000000';
}
if (schema.format === 'date-time') {
return '2025-01-01T00:00:00Z';
}
if (schema.enum) {
return schema.enum[0];
}
return 'string';
}
if (schema.type === 'integer' || schema.type === 'number') {
return schema.minimum || 0;
}
if (schema.type === 'boolean') {
return true;
}
if (schema.type === 'array') {
return [generateSafeExample(schema.items)];
}
if (schema.type === 'object') {
const obj: any = {};
for (const [key, propSchema] of Object.entries(schema.properties || {})) {
obj[key] = generateSafeExample(propSchema);
}
return obj;
}
return null;
}
CI Check for Sensitive Data
// scripts/check-openapi-pii.ts
import yaml from 'js-yaml';
import fs from 'fs';
const PII_PATTERNS = [
/\b[A-Za-z0-9._%+-]+@(?!example\.com)[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/i, // Real emails
/\b\d{3}-\d{2}-\d{4}\b/, // SSN format (not 000-00-0000)
/\bsk_live_\w+/, // Live Stripe keys
/\b(password|secret|token)["']?\s*[:=]\s*["'][^"']+["']/i, // Credentials
/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, // Internal IPs
/\b10\.\d+\.\d+\.\d+/, // Private IPs
/\b192\.168\.\d+\.\d+/, // Private IPs
];
function checkForPII(obj: any, path: string = ''): string[] {
const issues: string[] = [];
if (typeof obj === 'string') {
for (const pattern of PII_PATTERNS) {
if (pattern.test(obj)) {
issues.push(`Potential PII at ${path}: "${obj.slice(0, 50)}..."`);
}
}
} else if (Array.isArray(obj)) {
obj.forEach((item, i) => {
issues.push(...checkForPII(item, `${path}[${i}]`));
});
} else if (typeof obj === 'object' && obj !== null) {
for (const [key, value] of Object.entries(obj)) {
issues.push(...checkForPII(value, `${path}.${key}`));
}
}
return issues;
}
// Run check
const spec = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'));
const issues = checkForPII(spec);
if (issues.length > 0) {
console.error('PII detected in OpenAPI spec:');
issues.forEach(issue => console.error(` - ${issue}`));
process.exit(1);
}
console.log('No PII detected in OpenAPI spec');
Securing Swagger UI
Disable in Production
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import swaggerDocument from './openapi.json';
const app = express();
// Only enable Swagger UI in development
if (process.env.NODE_ENV !== 'production') {
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
}
// In production, serve static docs or redirect to docs site
if (process.env.NODE_ENV === 'production') {
app.get('/docs', (req, res) => {
res.redirect('https://docs.example.com');
});
}
Authenticated Swagger UI
import basicAuth from 'express-basic-auth';
// Protect Swagger UI with authentication
const swaggerAuth = basicAuth({
users: { admin: process.env.SWAGGER_PASSWORD! },
challenge: true,
realm: 'API Documentation',
});
app.use('/docs', swaggerAuth, swaggerUi.serve, swaggerUi.setup(swaggerDocument));
Read-Only Mode (Disable Try It Out)
const swaggerOptions = {
swaggerOptions: {
supportedSubmitMethods: [], // Disable all "Try it out" methods
// Or limit to safe methods only:
// supportedSubmitMethods: ['get', 'head', 'options'],
},
};
app.use(
'/docs',
swaggerUi.serve,
swaggerUi.setup(swaggerDocument, swaggerOptions)
);
Access Control for Documentation
Tiered Access
interface DocsUser {
id: string;
tier: 'public' | 'partner' | 'internal';
}
const specs = {
public: publicOpenApiSpec,
partner: partnerOpenApiSpec,
internal: internalOpenApiSpec,
};
app.get('/openapi.json', authenticate, (req, res) => {
const user = req.user as DocsUser | undefined;
// Default to public spec for unauthenticated
const tier = user?.tier || 'public';
const spec = specs[tier];
res.json(spec);
});
app.use('/docs', authenticate, (req, res, next) => {
const user = req.user as DocsUser | undefined;
const tier = user?.tier || 'public';
// Serve Swagger UI with appropriate spec
swaggerUi.setup(specs[tier])(req, res, next);
});
Robots.txt and Noindex
# robots.txt
User-agent: *
Disallow: /docs/
Disallow: /swagger/
Disallow: /openapi.json
Disallow: /api-docs/
Disallow: /redoc/
<!-- Documentation pages -->
<meta name="robots" content="noindex, nofollow">
// Middleware to add noindex header
app.use('/docs', (req, res, next) => {
res.setHeader('X-Robots-Tag', 'noindex, nofollow');
next();
});
Security Scheme Documentation
Proper Security Scheme Examples
components:
securitySchemes:
# ✅ GOOD - Clear without over-revealing
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT token obtained from /auth/login endpoint.
Include in Authorization header: `Bearer <token>`
# ✅ GOOD - API Key documentation
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: |
API key provided in your developer dashboard.
Contact [email protected] to request access.
# ❌ BAD - Too much detail
OAuth2Bad:
type: oauth2
description: |
OAuth2 with authorization code flow.
Client secret is validated using HMAC-SHA256.
Tokens are stored in Redis with 1-hour TTL.
Bypass rate limiting by setting X-Internal: true header.
Error Message Sanitization
# OpenAPI error response documentation
components:
schemas:
# ✅ GOOD - Generic error without implementation details
Error:
type: object
properties:
error:
type: string
description: Error code
example: "VALIDATION_ERROR"
message:
type: string
description: Human-readable message
example: "Invalid email format"
request_id:
type: string
description: Correlation ID for support
example: "req_abc123"
# ❌ BAD - Exposes implementation
BadError:
type: object
properties:
error:
example: "MongoError: E11000 duplicate key error"
stack:
example: "at UserModel.save (/app/src/models/user.js:45:12)"
query:
example: "SELECT * FROM users WHERE id = '1; DROP TABLE users;--'"
Documentation Review Checklist
┌─────────────────────────────────────────────────────────────────────────────┐
│ DOCUMENTATION SECURITY CHECKLIST │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ SENSITIVE ENDPOINTS │
│ [ ] Admin endpoints not in public docs │
│ [ ] Debug/test endpoints excluded │
│ [ ] Internal-only routes filtered │
│ [ ] Deprecated endpoints marked (not hidden) │
│ │
│ EXAMPLE DATA │
│ [ ] No real email addresses │
│ [ ] No real phone numbers │
│ [ ] No real addresses or locations │
│ [ ] No real API keys or tokens │
│ [ ] No real user IDs or account numbers │
│ [ ] Placeholder tokens clearly fake │
│ │
│ SECURITY DETAILS │
│ [ ] No implementation details exposed │
│ [ ] No version numbers of dependencies │
│ [ ] No internal hostnames or IPs │
│ [ ] Rate limits documented generically │
│ [ ] Error formats don't reveal internals │
│ │
│ ACCESS CONTROL │
│ [ ] Swagger UI disabled or protected in prod │
│ [ ] OpenAPI spec access controlled │
│ [ ] Documentation pages have noindex │
│ [ ] robots.txt blocks crawlers │
│ │
│ REVIEW PROCESS │
│ [ ] Security review before publishing │
│ [ ] CI checks for PII in examples │
│ [ ] Separate specs for different audiences │
│ [ ] Regular audits of published docs │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Best Practices
- Separate specs - Different detail levels for different audiences
- Filter at build time - Remove internal endpoints before publishing
- Fake examples - Obviously fake data in all examples
- Automate checks - CI pipeline scans for PII
- Protect Swagger UI - Auth or disable in production
- Block crawlers - robots.txt and noindex
- Minimal security details - Document what, not how
- Review process - Security review before publishing
- Version consistency - Docs match deployed API version
- Monitor exposure - Alert on indexed documentation pages
Next Steps
- API Security Complete Guide - Comprehensive security overview
- API Versioning Strategies - Version your API properly
- API Penetration Testing - Test your API security
- API Gateway Security - Gateway-level protection