## https://sploitus.com/exploit?id=436E852F-DAC6-5315-9EF9-44B559F5FF21
/ URLs remain to be filled. It still ships a SAFE
reproducer only โ no turnkey file-read exploit, no primary evidence with secrets โ because unpatched
sites remain in the wild.
-->
# Drupal OpenAI Provider โ SSRF / local file read via response URL (CVE-2026-13233)
*Untrusted upstream: the provider that fetched the URL its response told it to.*
Drupal **OpenAI Provider** (`ai_provider_openai`) ยท **SSRF** (CWE-918) ยท **CVE-2026-13233** ยท **SA-CONTRIB-2026-053**
> **Status:** Coordinated disclosure **complete (2026-07-10).** Reported privately to the Drupal Security
> Team, fixed, and published as **CVE-2026-13233** / advisory **SA-CONTRIB-2026-053** (rated *Moderately
> critical*). Author credited as **finder & remediation developer**.
A study of a **classic authorization / input-validation defect on an AI "connection point"**: an
AI image-generation provider fetched the URL contained in the **upstream API response** using a
general-purpose function with **no scheme allowlist**. On deployments where the configured upstream
is not the trusted default (bring-your-own-endpoint / proxy / self-hosted gateway / compromised
relay), the response could point the server at `file://` or an internal address.
**The untrusted input that reaches the sink is the upstream response URL โ not the user's prompt.**
- **Class (published):** SSRF (**CWE-918**). The SSRF included a `file://` **local file read**; the author's
internal analysis also mapped it to CWE-73 / CWE-441.
- **Severity:** Drupal ***Moderately critical*** (SA-CONTRIB-2026-053) โ a mid band on Drupal's scale โ CVSS
Medium; **not CVSS Critical/High.** Author's pre-disclosure self-assessment: floor `3.1` / conditional `5.3`
(Medium); prior 6.5 / 7.4 **retracted**, and the mid rating bore that out.
- **Affected:** Drupal **OpenAI Provider** (`ai_provider_openai`) ** **Docs are currently Japanese** (`.ja.md`); English canonical translations are a TODO. **Withheld from
> this repo:** arming payloads and any turnkey file-read PoC. The public `reproducer/` demonstrates the
> *defect mechanism* against a mock using a **single benign oracle** (`/etc/hostname`) โ no arbitrary-path
> read, no `settings.php` grab, no IMDS. It is not a weapon against live sites. Update to **1.1.1 / 1.2.2**.
---
## Safe Reproducer
**Goal:** let a researcher confirm the defect *class* without providing a live-target exploit or exposing
secrets. Everything runs offline.
- **Isolation:** `docker compose` with the app and a mock upstream on an isolated network (`--network none`
for the language-primitive checks). No credentials mounted. The mock never talks to the real API.
- **Faithful-untrusted-upstream model:** the mock plays "a `host` pointing at an untrusted proxy/self-hosted
gateway." It is armed **out-of-band** (not via the prompt) to return a synthetic target URL โ this encodes
the corrected attacker model (the *upstream* chooses the URL, the user only triggers).
- **Benign oracles only:**
- `file:///etc/hostname` and a **per-run unique marker** written *inside the app container only* โ proves
the app read an **upstream-chosen** path (marker absent on the mock).
- `settings.php` verified by **sha256 byte-match**, content never emitted โ proves "DB creds in scope"
without disclosing them.
- An internal HTTP service with **no public port** โ proves internal SSRF response retrieval with a
synthetic token.
- **Verdict is raw-byte, not HTTP 200:** success = served body **byte-equals** the expected synthetic value,
plus a **200-vs-403** authorization contrast. Exit codes are strict (`0` exploitable / `1` precondition /
`2` not-reproduced / `3` cleanup-failed / `99` error). See [`reproducer/verdict.md`](reproducer/verdict.md).
- **Transparency note:** any environment normalization (e.g. correcting a sandbox files-directory owner that
a prior root-run left unwritable) is documented and affects only the *save/serve* leg, **not** the file-read
sink itself. This is disclosed so reviewers don't misread it as a rigged precondition.
See [`reproducer/README.md`](reproducer/README.md).
---
## Disclosure Timeline
See [`timeline.md`](timeline.md). Summary:
| Date | Event |
|---|---|
| 2026-06-10 | Static review locates the sink; fixed-path `file://` read dynamically confirmed |
| 2026-06-11 | Arbitrary-path read + `settings.php` sha256 match + internal HTTP SSRF confirmed |
| 2026-06-11 | Production-consumer reachability (automator internal-path; FWA legitimate-HTTP exfil) |
| 2026-06-11 | Independent peer review corrects over-claims (attacker model, CVSS, `allow_url_fopen`); submission put on HOLD |
| 2026-06/07 | HOLD items cleared; reported privately to the Drupal Security Team; base-class twin flagged to drupal/ai |
| 2026-07-10 | Fixed releases **1.1.1 / 1.2.2**; advisory **SA-CONTRIB-2026-053** and **CVE-2026-13233** published; author credited as **finder & remediation developer** |
---
## Detection Guidance
Practitioner detections live in [`detections/`](detections/); rationale in
[`docs/detection-engineering.md`](docs/detection-engineering.md). In short:
- Correlate **who triggered image generation** (app audit) with **anomalous egress** (proxy / EDR) to
`169.254.169.254` / loopback / RFC1918 in a **tight time window** after the API call. Do not alert on
egress alone.
- `file://` reads never hit the network โ detect **sensitive-file opens by web processes** via host auditing
(auditd/EDR), baselined so only off-pattern reads within an image-gen request fire.
- Retrospectively hunt **`image/*`-declared bytes whose magic bytes aren't an image**.
- Most AI integrations don't log **which user fetched which URL** โ add a fetch audit record
`{request_id, actor, target_url, scheme, resolved_ip, content_type}`. Standard logs alone won't catch this.
---
## References
- OWASP Top 10 **A10:2021 โ Server-Side Request Forgery**
- **CWE-918** SSRF ยท **CWE-73** external control of file name/path ยท **CWE-441** unintended proxy/relay ยท **CWE-610** externally controlled reference
- PHP stream wrappers (`file://` is the default filesystem wrapper; not gated by `allow_url_fopen`)
- Drupal Security Team โ coordinated disclosure via the project's "Report a security vulnerability"
- **Advisory:** Drupal SA-CONTRIB-2026-053 โ https://www.drupal.org/sa-contrib-2026-053
- **CVE:** CVE-2026-13233 (CNA: Drupal.org)
---
## Security Notice
- Disclosure is complete (**CVE-2026-13233 / SA-CONTRIB-2026-053**), so product/version/code details are public.
**Update to 1.1.1 / 1.2.2 first** โ unpatched sites remain in the wild.
- The `reproducer/` is **sandbox-only** and uses **synthetic oracles**. It is **not** a live-target exploit and
contains **no secrets**. Do not run it against systems you do not own and are not authorized to test.
- No working file-read PoC and no primary evidence containing secrets are included here โ deliberately, even
post-fix, while unpatched deployments exist.
## Responsible Disclosure Disclaimer
All research was performed by the author in a self-owned, network-isolated sandbox, against a mock upstream โ
**the real vendor API was never contacted, and no third-party system was ever tested.** The finding was reported
privately to the Drupal Security Team first and published only in sync with the fix and advisory
(SA-CONTRIB-2026-053, CVE-2026-13233); the vendor's rating is *Moderately critical*. The author's CVSS figures
are a **provisional self-assessment**, not an official score, and were deliberately kept conservative (no
CVSS Critical/High) โ consistent with the vendor's mid-band rating. This material is for defensive and
educational purposes. See [`SECURITY.md`](SECURITY.md).