Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-poets-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swagger-typescript-api": patch
---

Fix JSDoc comment security vulnerability: escape only necessary `*/` sequences to prevent code injection while reducing noise in generated documentation
2 changes: 2 additions & 0 deletions src/code-gen-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ export class CodeGenProcess {
Ts: this.config.Ts,
formatDescription:
this.schemaParserFabric.schemaFormatters.formatDescription,
escapeJSDocContent:
this.schemaParserFabric.schemaFormatters.escapeJSDocContent,
internalCase: internalCase,
classNameCase: pascalCase,
pascalCase: pascalCase,
Expand Down
18 changes: 14 additions & 4 deletions src/schema-parser/schema-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,28 @@ export class SchemaFormatters {
return formatterFn?.(parsedSchema) || parsedSchema;
};

escapeJSDocContent = (content) => {
if (!content) return "";
// Escape */ sequences to prevent breaking out of JSDoc comments
// Note: /* sequences inside JSDoc comments are harmless and don't need escaping
return content.replace(/\*\//g, "*\\/");
};

formatDescription = (description, inline) => {
if (!description) return "";

const hasMultipleLines = description.includes("\n");
// Always escape JSDoc comment characters (function is idempotent)
const escapedDescription = this.escapeJSDocContent(description);

const hasMultipleLines = escapedDescription.includes("\n");

if (!hasMultipleLines) return description;
if (!hasMultipleLines) return escapedDescription;

if (inline) {
return (
lodash
// @ts-expect-error TS(2339) FIXME: Property '_' does not exist on type 'LoDashStatic'... Remove this comment to see the full error message
._(description)
._(escapedDescription)
.split(/\n/g)
.map((part) => part.trim())
.compact()
Expand All @@ -122,7 +132,7 @@ export class SchemaFormatters {
);
}

return description.replace(/\n$/g, "");
return escapedDescription.replace(/\n$/g, "");
};

formatObjectContent = (content) => {
Expand Down
11 changes: 6 additions & 5 deletions templates/base/object-field-jsdoc.ejs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@

<%
const { field, utils } = it;
const { formatDescription, require, _ } = utils;
const { formatDescription, escapeJSDocContent, require, _ } = utils;

const comments = _.uniq(
_.compact([
field.title,
field.description,
field.title && escapeJSDocContent(field.title),
field.description && escapeJSDocContent(field.description),
field.deprecated && ` * @deprecated`,
!_.isUndefined(field.format) && `@format ${field.format}`,
!_.isUndefined(field.minimum) && `@min ${field.minimum}`,
!_.isUndefined(field.maximum) && `@max ${field.maximum}`,
!_.isUndefined(field.pattern) && `@pattern ${field.pattern}`,
!_.isUndefined(field.pattern) && `@pattern ${escapeJSDocContent(field.pattern)}`,
!_.isUndefined(field.example) &&
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`,
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : escapeJSDocContent(field.example)}`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSDoc Comment Injection Vulnerability

When field.example is an object, its JSON.stringify output isn't escaped. This allows */ sequences within the stringified content to prematurely close JSDoc comments, potentially enabling comment injection.

Fix in Cursor Fix in Web

]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []),
);
%>
Expand Down
6 changes: 3 additions & 3 deletions templates/base/route-docs.ejs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<%
const { config, route, utils } = it;
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
const { _, formatDescription, escapeJSDocContent, fmtToJSDocLine, pascalCase, require } = utils;
const { raw, request, routeName } = route;

const jsDocDescription = raw.description ?
Expand All @@ -9,7 +9,7 @@ const jsDocDescription = raw.description ?
const jsDocLines = _.compact([
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
` * @name ${pascalCase(routeName.usage)}`,
raw.summary && ` * @summary ${raw.summary}`,
raw.summary && ` * @summary ${formatDescription(raw.summary, true)}`,
` * @request ${_.upperCase(request.method)}:${raw.route}`,
raw.deprecated && ` * @deprecated`,
routeName.duplicate && ` * @originalName ${routeName.original}`,
Expand All @@ -18,7 +18,7 @@ const jsDocLines = _.compact([
...(config.generateResponses && raw.responsesTypes.length
? raw.responsesTypes.map(
({ type, status, description, isSuccess }) =>
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${formatDescription(description, true)}`,
)
: []),
]).map(str => str.trimEnd()).join("\n");
Expand Down
Loading
Loading