# REPRO-2026-00251: Premmerce Wishlist for WooCommerce unauthenticated SQL injection ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00251 CVE: CVE-2026-54849 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # RCA Report — CVE-2026-54849 ## Summary Unauthenticated SQL injection exists in the **Premmerce Wishlist for WooCommerce** WordPress plugin (versions `<= 1.1.11`). The plugin persists a visitor's wishlist "keys" in a browser cookie named `premmerce_wishlist` (a JSON array). On every unauthenticated request to the wishlist functionality, the plugin reads that cookie, decodes it, and feeds the attacker-controlled keys directly into a SQL `IN (...)` query built by **string concatenation** (`implode('", "', $keys)`). Because the keys are never validated or escaped before being interpolated, an unauthenticated attacker can break out of the double-quoted literal and inject arbitrary SQL. The injection is reachable through an unauthenticated REST API endpoint (`/?rest_route=/premmerce/wishlist/add/popup`, registered with `permission_callback => __return_true`), so no WordPress login is required. ## Impact - **Package/component affected:** `premmerce-woocommerce-wishlist` (Premmerce Wishlist for WooCommerce). Vulnerable component: `src/WishlistStorage.php::cookieGet()` + `src/Models/WishlistModel.php::getWishlistsByKeys()` / `getDefaultWishlistByKeys()`. - **Affected versions:** `<= 1.1.11`. Patched in `1.1.12` (changelog: *"Security: SQL injection fix in wishlist cookie handling"*). - **Risk level:** Critical (Patchstack CVSS 9.3, AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L). - **Consequences:** An unauthenticated remote attacker can interact with the WordPress database via SQL injection — e.g. exfiltrate data from any table (demonstrated below by extracting the admin `user_login` from `wp_users` via a `UNION SELECT`) and perform time-based blind inference. This can lead to full disclosure of the WordPress database (users, password hashes, sessions, etc.). ## Impact Parity - **Disclosed/claimed maximum impact:** Unauthenticated SQL injection allowing interaction with the database, including data extraction (CVSS 9.3, C:H). - **Reproduced impact from this run:** Unauthenticated SQL injection proven on the real product through two independent techniques: 1. **Time-based blind SQLi** — a `SLEEP(5)` payload delayed the unauthenticated REST response by ~5.2–6.1s on the vulnerable build (vs ~0.1s on the fixed build). 2. **UNION-based data extraction** — a `UNION SELECT` payload leaked the WordPress admin `user_login` (`sqliadmin`) into the unauthenticated REST response body on the vulnerable build (1 occurrence in the response), with no leak on the fixed build. - **Parity:** `full`. The claimed unauthenticated SQL injection (data extraction) was reproduced end-to-end against the real product on the claimed `api_remote` surface. - **Not demonstrated:** This proof stops at database read/extraction (consistent with the disclosed C:H impact). It does not demonstrate writing to the database or code execution, which are not claimed by the advisory. ## Root Cause The cookie value is trusted all the way into the SQL query without validation or parameterization. Vulnerable `src/WishlistStorage.php::cookieGet()` (1.1.11): ```php public function cookieGet() { if ($this->cookieIsSet()) { $data = json_decode(stripslashes($_COOKIE[self::COOKIE_NAME])); return $data ? $data : array(); // <-- no validation of $data entries } return array(); } ``` (`COOKIE_NAME = 'premmerce_wishlist'`. `stripslashes` undoes the `addslashes` that WordPress applies to `$_COOKIE` via `wp_magic_quotes()` on every request, so `json_decode` effectively receives the raw, URL-decoded cookie value unchanged.) Vulnerable `src/Models/WishlistModel.php::getWishlistsByKeys()` (1.1.11): ```php public function getWishlistsByKeys($keys) { if ($keys && is_array($keys)) { $sql = vsprintf( "SELECT * FROM `%s` WHERE `wishlist_key` IN (%s);", array( $this->tblWishlist, '"' . implode('", "', $keys) . '"' // <-- direct concatenation ) ); return $this->getWishlistsBySql($sql); } } ``` The attacker-controlled `$keys` are placed inside the `IN ("...")` list with only a double-quote wrapper. A key containing `x") UNION SELECT ...` closes the quote and injects SQL. The same concatenation pattern exists in `getDefaultWishlistByKeys()`, `setUserToWishlistsByKeys()`, `deleteWishlists()`, `getWishlistsByProductId()`, and the `where_in` branch of `getWishlists()`. **Reachability (unauthenticated):** `src/RestApi/RestApi.php` registers `register_rest_route('premmerce/wishlist', '/add/popup', ... 'permission_callback' => '__return_true')`. Its callback `wishlistAddPopupRest()` → `wishlistAddPopup()` → `getWishlistsAll()`: ```php public function getWishlistsAll() { if (is_user_logged_in()) { ... } else { if ($this->storage->cookieIsSet()) { $wishlistAll = $this->model->getWishlistsByKeys($this->storage->cookieGet()); // <-- SQLi } } return $wishlistAll; } ``` So an unauthenticated visitor supplying a crafted `premmerce_wishlist` cookie reaches the vulnerable query. (WooCommerce must be active for the plugin's REST/frontend to register, via `WishlistPlugin::run()` / `validateRequiredPlugins()`.) **Fix (1.1.12):** `cookieGet()` now validates each key with `preg_match('/^[a-zA-Z0-9]{1,13}$/', $key)` (rejecting anything containing quotes, spaces, SQL metacharacters) and the SQL builders were converted to `$wpdb->prepare()` with `%s` placeholders and an `allowed_columns` allow-list for `where_in`. The fix diff (`src/WishlistStorage.php` + `src/Models/WishlistModel.php`) is the authoritative patch. ### Injected SQL (this reproduction) The crafted cookie URL-decodes to the JSON value `["x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- "]` (the embedded `"` is JSON-escaped as `\"`). After `json_decode`, the key is `x") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- `, which is concatenated into: ```sql SELECT * FROM `wp_premmerce_wishlist` WHERE `wishlist_key` IN ("x") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- "); ``` The `wp_premmerce_wishlist` table has 8 columns (`ID, user_id, name, wishlist_key, products, date_created, date_modified, default`), so the `UNION SELECT` supplies 8 columns. The data-extraction variant `["x\") UNION SELECT 1,2,user_login,4,5,6,7,8 FROM wp_users-- "]` places `wp_users.user_login` into the `name` column, which the wishlist popup template renders — leaking the admin username into the unauthenticated response. ## Reproduction Steps 1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained; run twice, both pass with exit 0). 2. **What it does:** - Deploys a real stack in Docker: `mariadb:11.4` + `wordpress:6.7.2-php8.2-apache`, installs WordPress (admin `sqliadmin`), activates WooCommerce 8.9.3, and installs+activates Premmerce Wishlist **1.1.11 (vulnerable)** from the local zip. - Sends an **unauthenticated** HTTP GET (from a separate client container on the Docker network, hitting the WordPress service by container name) to `/?rest_route=/premmerce/wishlist/add/popup` with the crafted `premmerce_wishlist` cookie. Two techniques, two attempts each: - **SLEEP payload** — measures response time (expect ~5s on vulnerable). - **UNION payload** — greps the response body for the admin `user_login` (expect a leak on vulnerable). - Swaps to the **fixed 1.1.12** build and repeats the same two techniques as a negative control (expect no sleep, no leak). - Writes `bundle/repro/runtime_manifest.json` and exits 0 only if all four criteria hold (vuln sleeps + leaks; fixed does neither). 3. **Expected evidence of reproduction:** - Vulnerable `SLEEP` response times ≈ 5.2–6.1s; fixed ≈ 0.07–0.13s. - Vulnerable `UNION` response bodies contain `sqliadmin`; fixed bodies do not. - `bundle/repro/runtime_manifest.json` with `confirmation_status: "confirmed"`. ## Evidence - **Main log:** `bundle/logs/reproduction_steps.log` (full script output, both runs). - **Runtime manifest:** `bundle/repro/runtime_manifest.json` (valid JSON). - **Response bodies:** `bundle/repro/artifacts/vuln_sleep_{1,2}.body`, `vuln_union_{1,2}.body`, `fixed_sleep_{1,2}.body`, `fixed_union_{1,2}.body`. - **Plugin source (vulnerable + fixed, for diff):** `bundle/artifacts/premmerce-1.1.11.zip`, `bundle/artifacts/premmerce-1.1.12.zip`. Key excerpt — **UNION data extraction on vulnerable 1.1.11** (admin username leaked into the unauthenticated REST response, inside the wishlist-name span): ``` $ grep -o '.\{0,40\}sqliadmin.\{0,40\}' bundle/repro/artifacts/vuln_union_1.body x-title\">\\n sqliadmin <\/span> $ grep -c sqliadmin bundle/repro/artifacts/vuln_union_1.body -> 1 $ grep -c sqliadmin bundle/repro/artifacts/fixed_union_1.body -> 0 ``` Key excerpt — **time-based blind SQLi divergence** (from `runtime_manifest.json`): ``` vulnerable_sleep_seconds: [5.197395, 6.098308] # SLEEP(5) fired fixed_sleep_seconds: [0.121983, 0.076013] # no sleep (regex filtered the key) vulnerable_union_leak_counts: [1, 1] # admin user_login leaked fixed_union_leak_counts: [0, 0] # no leak ``` **Environment:** Docker `mariadb:11.4` + `wordpress:6.7.2-php8.2-apache` (PHP 8.2.28), WooCommerce 8.9.3, Premmerce Wishlist 1.1.11 (vulnerable) / 1.1.12 (fixed), WordPress table prefix `wp_`, admin `sqliadmin`. The unauthenticated HTTP requests are issued by a separate container on the Docker bridge network to the WordPress service by container name (`http://premmerce-wp/...`); response bodies are retrieved via `docker create` + `docker cp` (host bind-mounts and published ports are not visible from this shell because the Docker daemon runs in a separate namespace). ## Recommendations / Next Steps - **Upgrade** Premmerce Wishlist for WooCommerce to **>= 1.1.12** immediately. - **Long-term:** the plugin should use `$wpdb->prepare()` with placeholders for *all* dynamic SQL (several other builders in `WishlistModel.php` were also concatenating user-influenced values and were patched in 1.1.12) and treat cookie data as untrusted, validating structure/types before use. - **Testing:** add automated tests that feed malicious cookie values (quotes, UNION, SLEEP, semicolons) and assert no SQL injection / no error / no data leak. Consider enabling a WAF rule for the `premmerce_wishlist` cookie. - **Detection:** review access logs for `premmerce_wishlist` cookie values containing `UNION`, `SLEEP`, `--`, `"`, or `)` as indicators of attempted exploitation. ## Additional Notes - **Idempotency:** the script tears down and rebuilds the whole stack on every run, so it can be executed repeatedly with deterministic results. Verified by two consecutive runs (both exit 0, both `confirmed`). - **Surface match:** the claimed surface is `api_remote` (unauthenticated HTTP request to the plugin endpoint); the proof uses a real unauthenticated HTTP request to the plugin's REST endpoint — `validated_surface = api_remote`. - **Scope/impact:** this is a data-extraction SQL injection (read). The proof does not demonstrate DB writes or RCE, which are outside the disclosed impact. - **Sanitizer:** no sanitizer is used; the proof is a plain non-sanitized product run (real Apache + WordPress + MariaDB), and the oracle is response timing + response content, not ASAN/UBSAN. ## Reproduction Details Reproduced: 2026-07-06T09:01:34.837Z Duration: 1646 seconds Tool calls: 177 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00251 pruva-verify CVE-2026-54849 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00251&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00251/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-54849 - Source: wordpress.org/plugins/premmerce-woocommerce-wishlist ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 13914 bytes) - bundle/repro/rca_report.md (analysis, 11484 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 19427 bytes) - bundle/vuln_variant/rca_report.md (analysis, 14730 bytes) - bundle/artifact_promotion_manifest.json (other, 16469 bytes) - bundle/logs/vuln_variant_reproduction.log (log, 10317 bytes) - bundle/vuln_variant/source_identity.json (other, 3038 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 3467 bytes) - bundle/repro/validation_verdict.json (other, 1039 bytes) - bundle/repro/runtime_manifest.json (other, 1283 bytes) - bundle/repro/artifacts/vuln_union_1.body (other, 4682 bytes) - bundle/repro/artifacts/fixed_union_1.body (other, 4152 bytes) - bundle/repro/artifacts/vuln_sleep_1.body (other, 4674 bytes) - bundle/repro/artifacts/fixed_sleep_1.body (other, 4152 bytes) - bundle/logs/reproduction_steps.log (log, 6306 bytes) - bundle/repro/artifacts/fixed_sleep_2.body (other, 4152 bytes) - bundle/repro/artifacts/fixed_union_2.body (other, 4152 bytes) - bundle/repro/artifacts/vuln_sleep_2.body (other, 4674 bytes) - bundle/repro/artifacts/vuln_union_2.body (other, 4682 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 2373 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2066 bytes) - bundle/vuln_variant/artifacts/c1_vuln.body (other, 4430 bytes) - bundle/vuln_variant/artifacts/c1_fixed.body (other, 3918 bytes) - bundle/vuln_variant/artifacts/c3_vuln.body (other, 52484 bytes) - bundle/vuln_variant/artifacts/c3_fixed.body (other, 50857 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 9539 bytes) - bundle/vuln_variant/variant_manifest.json (other, 6799 bytes) - bundle/vuln_variant/artifacts/c0_fixed.body (other, 4152 bytes) - bundle/vuln_variant/artifacts/c0_latest.body (other, 4152 bytes) - bundle/vuln_variant/artifacts/c0_vuln.body (other, 4674 bytes) - bundle/vuln_variant/artifacts/c1_latest.body (other, 3918 bytes) - bundle/vuln_variant/artifacts/c2_fixed.body (other, 0 bytes) - bundle/vuln_variant/artifacts/c2_latest.body (other, 0 bytes) - bundle/vuln_variant/artifacts/c2_vuln.body (other, 0 bytes) - bundle/vuln_variant/artifacts/c3_latest.body (other, 50857 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00251 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00251/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00251 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev