Share
## 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).