# Craft Commerce has a SQL Injection can lead to Remote Code Execution via TotalRevenue Widget

SQL injection in Craft Commerce TotalRevenue widget settings enables stacked INSERT of a serialized Guzzle FileCookieJar gadget into the queue table, yielding webshell RCE.

- Payout: Research
- Severity: high
- Program: Craft Commerce
- Found by: RajChowdhury240, rlarabee
- Disclosed: 2026-04-14
- CVE: CVE-2026-32271
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-875v-7m49-8x88
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-875v-7m49-8x88
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:deserialization vuln:rce vuln:sqli

**Target:** Craft CMS control panel dashboard widget endpoint and queue runner
**Root cause:** The TotalRevenue widget interpolates unsanitized settings into a sprintf-based SQL Expression, and any control panel user can create any widget type without permission checks. PDO MySQL multi-statements plus unrestricted yii2-queue unserialize() turn the SQLi into object injection.
**Steps:**
1. Authenticate as any control panel user
2. POST to `/admin/actions/dashboard/create-widget` with `settings[type]` containing a stacked INSERT writing a serialized FileCookieJar gadget chain into the queue table
3. Observe HTTP 500 (expected; INSERT already committed)
4. Trigger queue processing via `GET /actions/queue/run` (anonymous-allowed)
5. Queue consumer deserializes gadget; `FileCookieJar::__destruct()` writes webshell to webroot
6. Access `GET /poc_rce.php?c=id` and observe `uid=1000(home) gid=1000(home) groups=1000(home)`
**Exploitation:** Any authenticated CP user creates a widget whose settings[type] carries a stacked INSERT of a serialized Guzzle FileCookieJar payload into the queue table. The unauthenticated /actions/queue/run endpoint processes the job, unserializes the object, and FileCookieJar::__destruct() writes a PHP webshell to webroot via file_put_contents(); PHP tags survive json_encode because Guzzle uses options=0. Three HTTP requests yield command execution as the PHP process user.
**Escalation:** Chained SQLi -> PDO multi-statement INSERT -> unrestricted unserialize -> Guzzle FileCookieJar gadget -> webshell write, escalating from CP-user SQLi to unauthenticated-triggered RCE.
**Insight:** Widget/stat settings interpolated into SQL Expressions are a recurring sink; check whether PDO multi-statements are enabled so a SELECT injection can stack INSERTs into queue/job tables, and look for __destruct file-write gadgets (Guzzle FileCookieJar) reachable via unrestricted unserialize in queue serializers.

