CVE-2026-54849: Verified Repro With Script Download
CVE-2026-54849: Premmerce Wishlist for WooCommerce unauthenticated SQL injection
CVE-2026-54849 is verified against the affected target. Vulnerability class: SQLi. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00251.
What Is CVE-2026-54849?
CVE-2026-54849 is a critical, unauthenticated SQL injection vulnerability (CWE-89) in the Premmerce Wishlist for WooCommerce WordPress plugin, versions <= 1.1.11. Pruva reproduced it (reproduction REPRO-2026-00251).
CVE-2026-54849 Severity & CVSS Score
CVE-2026-54849 is rated critical severity, with a CVSS base score of 9.3 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
How to Reproduce CVE-2026-54849
pruva-verify REPRO-2026-00251 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00251/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-54849
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
premmerce_wishlist cookie (JSON array of wishlist keys) containing an embedded SQL injection payload (e.g. x") UNION SELECT ...)
- Unauthenticated HTTP GET to /?rest_route=/premmerce/wishlist/add/popup (permission_callback __return_true) with crafted premmerce_wishlist cookie
- RestApi::wishlistAddPopupRest
- getWishlistsAll()
- WishlistModel::getWishlistsByKeys(WishlistStorage::cookieGet())
- SQL built by string concatenation of cookie keys into IN ("...")
Variant/bypass search for CVE-2026-54849 (Premmerce Wishlist unauthenticated SQLi via premmerce_wishlist cookie). Four materially-distinct unauthenticated entry points to the same cookie-derived concatenation sink were tested (REST /add/popup GET, wc-ajax premmerce_wishlist_popup GET, REST /add POST reaching getDefaul…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-54849
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.
- 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 in1.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_loginfromwp_usersvia aUNION 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:
- 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). - UNION-based data extraction — a
UNION SELECTpayload leaked the WordPress adminuser_login(sqliadmin) into the unauthenticated REST response body on the vulnerable build (1 occurrence in the response), with no leak on the fixed build.
- Time-based blind SQLi — a
- Parity:
full. The claimed unauthenticated SQL injection (data extraction) was reproduced end-to-end against the real product on the claimedapi_remotesurface. - 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):
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):
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():
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:
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
- Script:
bundle/repro/reproduction_steps.sh(self-contained; run twice, both pass with exit 0). - What it does:
- Deploys a real stack in Docker:
mariadb:11.4+wordpress:6.7.2-php8.2-apache, installs WordPress (adminsqliadmin), 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/popupwith the craftedpremmerce_wishlistcookie. 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.jsonand exits 0 only if all four criteria hold (vuln sleeps + leaks; fixed does neither).
- Deploys a real stack in Docker:
- Expected evidence of reproduction:
- Vulnerable
SLEEPresponse times ≈ 5.2–6.1s; fixed ≈ 0.07–0.13s. - Vulnerable
UNIONresponse bodies containsqliadmin; fixed bodies do not. bundle/repro/runtime_manifest.jsonwithconfirmation_status: "confirmed".
- Vulnerable
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 inWishlistModel.phpwere 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_wishlistcookie. - Detection: review access logs for
premmerce_wishlistcookie values containingUNION,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.
Variant Analysis & Alternative Triggers for CVE-2026-54849
No bypass or distinct unauthenticated variant was found. The 1.1.12 fix for
CVE-2026-54849 is defense-in-depth and complete: it validates every
premmerce_wishlist cookie key with a strict allow-list regex
(^[a-zA-Z0-9]{1,13}$) at the single choke point
WishlistStorage::cookieGet(), and it parameterizes every dynamic SQL sink
in WishlistModel with $wpdb->prepare(). This variant analysis searched for
materially-distinct alternate entry points that reach the same cookie-derived
concatenation sink and tested them on the vulnerable (1.1.11), fixed (1.1.12),
and latest (1.1.13) builds. Four unauthenticated alternate triggers were
confirmed to exist on the vulnerable build (they all fire SLEEP(5),
~5–6 s) — proving the search was real, not fabricated — but none fire on the
fixed or latest build (all < 0.13 s). The fix covers every alternate path. A
fifth, authenticated alternate trigger (wp_login → setUserToWishlistsByKeys
UPDATE) was identified by source inspection and is likewise covered by the same
two-layer fix.
Fix Coverage / Assumptions
Invariant the fix relies on: all cookie→SQL data flows through
WishlistStorage::cookieGet(), which is the only place the
premmerce_wishlist cookie is decoded. The regex there is the outer guard; the
parameterized sinks are the inner guard.
Code paths explicitly covered:
cookieGet()— rejects any key containing quotes, spaces, parentheses, comments, or other SQL metacharacters (regex^[a-zA-Z0-9]{1,13}$); drops non-array / non-string JSON.WishlistModel::getWishlistsByKeys(),getDefaultWishlistByKeys(),setUserToWishlistsByKeys(),deleteWishlists(),getWishlistsByProductId(), andgetWishlists()(where_in + orderby) — all converted to$wpdb->prepare()with matched%s/%dplaceholders;getWishlists()where_in additionally restricts columns to anallowed_columnsallow-list (ID,user_id,wishlist_key).
What the fix does NOT cover: nothing unauthenticated. There are no
remaining concatenation sinks in the fixed WishlistModel (verified by
grep — the only implode() calls left build the %s,%s,... placeholder
string, never interpolate data). Request-parameter inputs on the unauthenticated
REST routes are all passed as prepare() arguments, and the URL-param routes
enforce [a-zA-Z0-9]{13} / \d+ regex constraints at the router level. The
admin list-table surfaces are authenticated (manage_options) and are not
injectable in the fixed build (and are out of scope for this unauthenticated
CVE).
Variant / Alternate Trigger
All candidates share the same root cause (untrusted premmerce_wishlist
cookie → cookieGet() → a string-concatenation IN (...) sink) and differ only
in the HTTP entry point that triggers it. They are alternate triggers of the
same bug, not a new bug.
| ID | Entry point (unauthenticated) | Code path → sink | Vuln 1.1.11 SLEEP | Fixed 1.1.12 | Latest 1.1.13 |
|---|---|---|---|---|---|
| C0 | GET /?rest_route=/premmerce/wishlist/add/popup |
RestApi::wishlistAddPopupRest → wishlistAddPopup → getWishlistsAll() → WishlistModel::getWishlistsByKeys(cookieGet()) |
5.20 s | 0.11 s | 0.12 s |
| C1 | GET /?wc-ajax=premmerce_wishlist_popup |
RestApi::wishlistAddPopupAjax → wishlistAddPopup → getWishlistsAll() → getWishlistsByKeys(cookieGet()) |
6.07 s | 0.04 s | 0.04 s |
| C2 | POST /?rest_route=/premmerce/wishlist/add |
RestApi::wishlistAdd → addProductToWishlist() → getDefaultWishlistByKeys(cookieGet()) (+ popup getWishlistsAll()) |
5.14 s | 0.08 s | 0.08 s |
| C3 | GET /?page_id=<wishlist_page> |
Frontend::wishlistPage (shortcode [wishlist_page]) → getWishlistsByKeys(cookieGet()) |
5.13 s | 0.10 s | 0.10 s |
| C4* | wp_login action |
Frontend::assignUserWishlistFromCookie → setUserToWishlistsByKeys($user->ID, cookieGet()) (UPDATE, single-quote IN ('...')) |
(source only) | covered | covered |
* C4 is an authenticated trigger (fires on any login). It reaches a
different sink (setUserToWishlistsByKeys, an UPDATE) with a different
quote breakout (single-quote) from the parent SELECT/double-quote. It is
covered by the same fix (cookieGet() regex + $wpdb->prepare() of
setUserToWishlistsByKeys). It was validated by source inspection rather than
runtime because the time-based oracle on an UPDATE ... WHERE is unreliable on
an empty wishlist table (the WHERE predicate is not evaluated when zero rows are
scanned).
Why these are not a bypass: every candidate funnels through the patched
cookieGet() and the now-parameterized sinks. Empirically, all four
unauthenticated candidates return in < 0.13 s on 1.1.12 and 1.1.13 (no SLEEP),
vs 5.1–6.1 s on 1.1.11.
- Package/component affected:
premmerce-woocommerce-wishlist(Premmerce Wishlist for WooCommerce). Vulnerable component:src/WishlistStorage.php::cookieGet()+src/Models/WishlistModel.phpconcatenation sinks; entry points insrc/RestApi/RestApi.php,src/Frontend/Frontend.php. - Affected versions (as tested):
<= 1.1.11vulnerable;1.1.12fixed;1.1.13latest (security-relevant code byte-identical to 1.1.12). - Risk level: Critical (Patchstack CVSS 9.3) for the parent vulnerability on
<= 1.1.11. No residual risk on the fixed/latest builds — no bypass. - Consequences of the parent bug: unauthenticated remote SQL injection (data extraction / time-based blind inference against the WordPress DB). The variant run did not introduce any new impact.
Impact Parity
- Disclosed/claimed maximum impact (parent): unauthenticated SQL injection with data extraction (CVSS 9.3, C:H).
- Reproduced impact from this variant run: the alternate entry points reproduce the same time-based blind SQLi on the vulnerable build (SLEEP ~5–6 s on all four unauthenticated candidates). On the fixed and latest builds, no injection is reproduced on any candidate.
- Parity (parent vs. variant on the vulnerable build):
full— the alternate triggers reach the same sink with the same payload and the same observable time-based oracle. - Parity (variant as a bypass of the fix):
none— no candidate injects on the fixed or latest build. - Not demonstrated: this run uses only the SLEEP time oracle to compare entry points across builds; it does not re-demonstrate the UNION-based data extraction (already proven in the parent repro on the baseline entry point). No DB write or RCE is demonstrated (out of the disclosed impact).
Root Cause
The same underlying bug — untrusted cookie data interpolated into SQL by
string concatenation — is reachable from multiple unauthenticated entry
points because they all call WishlistStorage::cookieGet() (no validation) and
then a WishlistModel concatenation sink (getWishlistsByKeys,
getDefaultWishlistByKeys, etc.). The 1.1.12 fix removes both halves of the bug:
(a) cookieGet() now validates each key with ^[a-zA-Z0-9]{1,13}$, and (b)
every sink is rebuilt with $wpdb->prepare(). Because both layers were fixed,
even an entry point that bypassed one layer would be stopped by the other.
No fix commit SHA is available (the plugin is distributed via WordPress.org as
release zips, not a public git repo); the fix is identified by the 1.1.12
release (premmerce-woocommerce-wishlist.1.1.12.zip,
sha256 103cbe3086ce0df927af7488e62f5b655d54094442b6385aeb6969d9068d822d),
changelog "Security: SQL injection fix in wishlist cookie handling".
Reproduction Steps
- Script:
bundle/vuln_variant/reproduction_steps.sh(self-contained, idempotent; run twice, both exit 1 = no bypass). - What it does:
- Deploys a real stack in Docker:
mariadb:11.4+wordpress:6.7.2-php8.2-apache, installs WordPress (adminsqliadmin), activates WooCommerce 8.9.3, enables pretty permalinks. - For each build — vulnerable 1.1.11, fixed 1.1.12, latest 1.1.13 — installs+activates Premmerce Wishlist from the local release zip and resolves the wishlist page id created on activation.
- Sends an unauthenticated HTTP request (from a separate client container
on the Docker network, hitting the WordPress service by container name)
carrying the crafted
premmerce_wishlistSLEEP cookie (identical payload to the parent repro:["x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- "]) to four candidate entry points: REST/add/popupGET (C0 baseline),wc-ajax=premmerce_wishlist_popupGET (C1), REST/addPOST (C2), and the frontend wishlist page GET (C3). Measures each response time. - Evaluates: a bypass = any candidate SLEEPs (≥ 4 s) on the fixed (1.1.12)
or latest (1.1.13) build. Writes
runtime_manifest.jsonandvalidation_verdict.json.
- Deploys a real stack in Docker:
- Expected evidence:
- Vulnerable 1.1.11: all four candidates ≈ 5.1–6.1 s (SLEEP fired) → real alternate triggers.
- Fixed 1.1.12 & latest 1.1.13: all four candidates < 0.13 s (no SLEEP) → fix covers them.
validation_verdict.jsonwithvariant_outcome: "no_bypass"; scriptexit 1.
Evidence
- Main log:
bundle/logs/vuln_variant_reproduction.log(full output of all runs, including per-candidatehttpcode +time_total). - Runtime manifest:
bundle/vuln_variant/runtime_manifest.json(confirmation_status: "no_bypass"; per-candidate timings on all three builds). - Validation verdict:
bundle/vuln_variant/validation_verdict.json(variant_outcome: "no_bypass",claim_block_reason: "fix_covers_all_alternate_entry_points"). - Response bodies:
bundle/vuln_variant/artifacts/c{0..3}_{vuln,fixed,latest}.body— confirm the endpoints were genuinely reached (C0/C1 render the wishlist popup HTML; C3 renders the full wishlist page HTML; C2 bodies are empty because/addPOST issues a redirect, but the SLEEP fired pre-redirect as shown by the 5.14 s timing on the vulnerable build). - Source identity:
bundle/vuln_variant/source_identity.json(release zips- sha256 + runtime-reported versions for 1.1.11 / 1.1.12 / 1.1.13).
- Root-cause equivalence:
bundle/vuln_variant/root_cause_equivalence.json. - Patch analysis:
bundle/vuln_variant/patch_analysis.md.
Key excerpt — per-candidate SLEEP divergence (from runtime_manifest.json):
candidate_labels: [c0_baseline_add_popup_GET, c1_wc_ajax_popup_GET, c2_rest_add_POST, c3_page_render_GET]
vulnerable_1111_sleep_seconds: [5.198, 6.068, 5.138, 5.125] # all four fire SLEEP
fixed_1112_sleep_seconds: [0.113, 0.035, 0.078, 0.100] # none fire (fix covers)
latest_1113_sleep_seconds: [0.113, 0.041, 0.079, 0.096] # none fire (latest covers)
vulnerable_sleep_count: 4/4 fixed_sleep_count: 0/4 latest_sleep_count: 0/4
bypass_on_fixed_or_latest: false
Environment: Docker mariadb:11.4 + wordpress:6.7.2-php8.2-apache
(PHP 8.2), WooCommerce 8.9.3, Premmerce Wishlist 1.1.11 / 1.1.12 / 1.1.13,
WordPress table prefix wp_, admin sqliadmin. Unauthenticated HTTP requests
issued by a separate container on the Docker bridge network to the WordPress
service by container name (http://premmerce-wp/...); response bodies 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
- No fix extension is required for CVE-2026-54849. The 1.1.12 fix already
covers every materially-distinct unauthenticated entry point (verified
empirically: C0–C3) and the authenticated
wp_loginUPDATE path (C4, by source inspection). The two-layer design (input validation + universal parameterization) is robust. - Generic hardening (low priority):
- Add automated regression tests that feed malicious
premmerce_wishlistcookie values (quotes,UNION,SLEEP,--,), single-quote variants) to every registered REST route and thewc-ajax/ page-render entry points and assert no SQL injection / no error / no timing anomaly. - Treat all
$_COOKIE/$_REQUESTas untrusted throughout the plugin; validate structure and types before use (thecookieGet()regex is a good template). - Consider a WAF rule alerting on
premmerce_wishlistcookie values containingUNION,SLEEP,--,",', or)as exploitation indicators (these are now blocked by the plugin, but the signal is useful for detection).
- Add automated regression tests that feed malicious
Additional Notes
- Idempotency: the script tears down and rebuilds the whole Docker stack on
every run, so it can be executed repeatedly with deterministic results.
Verified by multiple consecutive runs (all
exit 1, allno_bypass, bothruntime_manifest.jsonandvalidation_verdict.jsonwritten). - Negative result is the correct outcome. The candidate set was bounded by
the actual code: every unauthenticated path to a concatenation sink goes
through
cookieGet(), and there are no concatenation sinks left in the fixedWishlistModel. Inventing further "variants" beyond C0–C4 would just relabel the same cookie→cookieGet()→sink flow through sibling routes (e.g. the/page,/delete,/page/renameroutes use request-parameter inputs that are already parameterized and router-regex-constrained), which the anti- fabrication rules explicitly exclude. - C4 limitation: the
wp_login→setUserToWishlistsByKeysUPDATE path was not runtime-tested because a time-based SLEEP in anUPDATE ... WHEREpredicate does not fire on an empty table (no rows scanned). It is covered by source inspection:cookieGet()regex +$wpdb->prepare()ofsetUserToWishlistsByKeys. A reliable runtime oracle for this path would require pre-seeding a wishlist row (so the UPDATE scans a row) — left as a optional follow-up since the fix demonstrably covers it. - Threat model / SECURITY.md: the plugin ships no
SECURITY.mdor threat- model document. The vendor themselves classified the cookie SQLi as a security defect and shipped a fix (1.1.12 changelog), so unauthenticated cookie-driven SQL injection is unambiguously in scope; no documented limitation is being reframed here. - Scope/impact of this run: this run proves the fix is complete (no bypass) and that the alternate triggers are real (fire on the vulnerable build). It does not demonstrate DB writes or RCE, which are outside the disclosed impact.
CVE-2026-54849 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-54849
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-54849
FAQ: CVE-2026-54849
How does the CVE-2026-54849 SQL injection attack work?
How severe is CVE-2026-54849?
How can I reproduce CVE-2026-54849?
Does exploiting CVE-2026-54849 require authentication?
References for CVE-2026-54849
Authoritative sources for CVE-2026-54849 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.