
CVE-2026-13233 (Drupal OpenAI Provider, SA-CONTRIB-2026-053): response-URL SSRF / local file read. Untrusted upstream, not the prompt. Safe reproducer + detections. Fixed in 1.1.1/1.2.2.
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.
file:// local file read; the author's
internal analysis also mapped it to CWE-73 / CWE-441.3.1 / conditional 5.3
(Medium); prior 6.5 / 7.4 retracted, and the mid rating bore that out.ai_provider_openai) < 1.1.1 and 1.2.0–1.2.1 (audited 1.2.1) on Drupal core 11.2All verification ran in an owned, network-isolated sandbox against a mock upstream. The real OpenAI API was never called. Leak oracles were synthetic only.
Confirmed (dynamically demonstrated):
settings.php read confirmed by sha256 byte-match (35,182 B) — content never printed; DB credentials
are therefore in scope.Unverified (explicitly not claimed):
Not affected: default api.openai.com over TLS. The host setting is admin / config-sync / deploy
controlled — a runtime non-admin attacker cannot change it.
The response URL was passed to a general-purpose fetch that honors every registered PHP stream
wrapper (file, php, http, data, phar, …). response_format defaults to url, so the vulnerable
branch is the default path. file:// is PHP's default filesystem wrapper and is not gated by
allow_url_fopen (an earlier "requires allow_url_fopen" claim was wrong and is retracted).
Fix the primitive, not just the call site:
http/https before the request; disable redirects (a 30x can bounce to file://).file://…, http://169.254.169.254/…, and a 302→file://
redirect must all be refused.Full direction: docs/fix-direction.md.
.
├── README.md # this file (English, canonical)
├── README_ja.md # Japanese mirror
├── SECURITY.md # reporting policy + responsible-disclosure disclaimer
├── .gitignore # blocks secrets / primary evidence from being committed
├── docs/
│ └── fix-direction.md # root-cause fix + variant hunt + minimal-diff sketch (EN)
├── reproducer/ # SAFE reproducer only (sandbox, mock upstream, benign oracle)
│ ├── README.md # how to stand up the network-isolated sandbox
│ ├── verdict.md # what "success" means: raw-byte match + 200-vs-403 + exit codes
│ ├── mock-openai-server.py # benign mock; default happy-path PNG, ssrf_demo → file:///etc/hostname only
│ └── docker-compose.yml # TEMPLATE: isolated Drupal sandbox + mock (you add the vulnerable target)
├── detections/
│ ├── README.md
│ ├── sigma/web-egress-to-internal-after-imagegen.yml
│ ├── sigma/php-sensitive-file-open.yml
│ ├── sentinel/imagegen-egress-correlation.kql
│ └── splunk/imagegen-content-type-mismatch.spl
└── timeline.md # disclosure timeline (single source of truth)
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 publicreproducer/demonstrates the defect mechanism against a mock using a single benign oracle (/etc/hostname) — no arbitrary-path read, nosettings.phpgrab, no IMDS. It is not a weapon against live sites. Update to 1.1.1 / 1.2.2.
Goal: let a researcher confirm the defect class without providing a live-target exploit or exposing secrets. Everything runs offline.
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.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).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.0 exploitable / 1 precondition /
2 not-reproduced / cleanup-failed / error). See .See reproducer/README.md.
See timeline.md. Summary:
Practitioner detections live in detections/; rationale in
docs/detection-engineering.md. In short:
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.image/*-declared bytes whose magic bytes aren't an image.{request_id, actor, target_url, scheme, resolved_ip, content_type}. Standard logs alone won't catch this.file:// is the default filesystem wrapper; not gated by allow_url_fopen)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.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.
399| 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 |