Home/Blog/Cybersecurity/API Documentation Security: Protecting Sensitive Information in OpenAPI Specs
Cybersecurity

API Documentation Security: Protecting Sensitive Information in OpenAPI Specs

Secure your API documentation by protecting sensitive endpoints, sanitizing examples, and implementing proper access controls for Swagger/OpenAPI specs.

By Inventive HQ Team
API Documentation Security: Protecting Sensitive Information in OpenAPI Specs

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

  1. Separate specs - Different detail levels for different audiences
  2. Filter at build time - Remove internal endpoints before publishing
  3. Fake examples - Obviously fake data in all examples
  4. Automate checks - CI pipeline scans for PII
  5. Protect Swagger UI - Auth or disable in production
  6. Block crawlers - robots.txt and noindex
  7. Minimal security details - Document what, not how
  8. Review process - Security review before publishing
  9. Version consistency - Docs match deployed API version
  10. Monitor exposure - Alert on indexed documentation pages

Next Steps

Need Expert Cybersecurity Guidance?

Our team of security experts is ready to help protect your business from evolving threats.