{
  "variant_id": "pruva-cve-2026-54849-variant-0001",
  "created_at": "2026-07-05T13:44:00Z",
  "variant_summary": "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 getDefaultWishlistByKeys, and the frontend wishlist page GET) plus one authenticated alternate trigger (wp_login -> setUserToWishlistsByKeys UPDATE) identified by source inspection. All unauthenticated candidates fire SLEEP on vulnerable 1.1.11 (real alternate triggers of the same root cause) but NONE fire on fixed 1.1.12 or latest 1.1.13. The 1.1.12 fix is defense-in-depth: cookieGet() validates keys with ^[a-zA-Z0-9]{1,13}$ AND every WishlistModel sink is parameterized with $wpdb->prepare(). No bypass found.",
  "relation": "newer_version_sibling",
  "origin_kind": "pruva_variant",
  "repository": "wordpress.org/plugins/premmerce-woocommerce-wishlist",
  "submitted_target": {
    "target_kind": "wordpress_plugin_release",
    "version": "1.1.11",
    "ref": "premmerce-woocommerce-wishlist.1.1.11",
    "display": "Premmerce Wishlist for WooCommerce 1.1.11 (vulnerable)"
  },
  "variant_target": {
    "target_kind": "wordpress_plugin_release",
    "version": "1.1.12",
    "ref": "premmerce-woocommerce-wishlist.1.1.12",
    "display": "Premmerce Wishlist for WooCommerce 1.1.12 (fixed) and 1.1.13 (latest) - security-relevant code byte-identical"
  },
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high",
  "claimed_surface": "api_remote",
  "validated_surface": "api_remote",
  "required_entrypoint_kind": "api_remote",
  "required_entrypoint_detail": "Unauthenticated HTTP request carrying a crafted premmerce_wishlist cookie to one of: REST /?rest_route=/premmerce/wishlist/add/popup (GET), /?wc-ajax=premmerce_wishlist_popup (GET), /?rest_route=/premmerce/wishlist/add (POST), or the frontend wishlist page (GET /?page_id=<id>). Issued from a separate Docker-network client container to the WordPress service by container name.",
  "attacker_controlled_input": "premmerce_wishlist cookie (JSON array) with an embedded key x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8--  (url-encoded). Identical payload to the parent repro for comparability.",
  "trigger_path": "cookie -> WishlistStorage::cookieGet() -> WishlistModel::getWishlistsByKeys()/getDefaultWishlistByKeys() (string-concatenation IN (\"...\") sink on 1.1.11). On 1.1.12/1.1.13 cookieGet() regex ^[a-zA-Z0-9]{1,13}$ drops the key and the sink is $wpdb->prepare()-parameterized, so no injection.",
  "observed_impact_class": "no_bypass_time_based_sqli_on_fixed",
  "exploitability_confidence": "high",
  "evidence_scope": "production_path",
  "runtime_manifest_present": true,
  "end_to_end_target_reached": true,
  "inferred": false,
  "claim_block_reason": "fix_covers_all_alternate_entry_points",
  "blocking_mitigation": "1.1.12 WishlistStorage::cookieGet() per-key regex ^[a-zA-Z0-9]{1,13}$ + full $wpdb->prepare() parameterization of every WishlistModel sink (getWishlistsByKeys, getDefaultWishlistByKeys, setUserToWishlistsByKeys, deleteWishlists, getWishlistsByProductId, getWishlists where_in allow-list). Covers all tested unauthenticated entry points and the wp_login assignUserWishlistFromCookie UPDATE path.",
  "file_path": "src/Models/WishlistModel.php",
  "line_start": 247,
  "line_end": 257,
  "secondary_anchors": [
    {
      "file_path": "src/WishlistStorage.php",
      "line_start": 57,
      "line_end": 67
    },
    {
      "file_path": "src/Models/WishlistModel.php",
      "line_start": 268,
      "line_end": 278
    },
    {
      "file_path": "src/Models/WishlistModel.php",
      "line_start": 591,
      "line_end": 601
    },
    {
      "file_path": "src/RestApi/RestApi.php",
      "line_start": 240,
      "line_end": 250
    },
    {
      "file_path": "src/Frontend/Frontend.php",
      "line_start": 149,
      "line_end": 170
    }
  ],
  "review_scope_paths": [
    "src/WishlistStorage.php",
    "src/Models/WishlistModel.php",
    "src/RestApi/RestApi.php",
    "src/Frontend/Frontend.php",
    "src/Frontend/WishlistFunctions.php",
    "src/Admin/WishlistTable.php"
  ],
  "candidate_results": {
    "c0_baseline_add_popup_GET": {
      "entry": "GET /?rest_route=/premmerce/wishlist/add/popup",
      "sink": "getWishlistsByKeys(cookieGet()) via getWishlistsAll()",
      "vuln_1_1_11_sleep_s": 5.19842,
      "fixed_1_1_12_sleep_s": 0.113164,
      "latest_1_1_13_sleep_s": 0.112547,
      "bypass": false
    },
    "c1_wc_ajax_popup_GET": {
      "entry": "GET /?wc-ajax=premmerce_wishlist_popup",
      "sink": "getWishlistsByKeys(cookieGet()) via getWishlistsAll()",
      "vuln_1_1_11_sleep_s": 6.068156,
      "fixed_1_1_12_sleep_s": 0.035061,
      "latest_1_1_13_sleep_s": 0.040686,
      "bypass": false
    },
    "c2_rest_add_POST": {
      "entry": "POST /?rest_route=/premmerce/wishlist/add",
      "sink": "getDefaultWishlistByKeys(cookieGet()) via addProductToWishlist() (+ getWishlistsAll())",
      "vuln_1_1_11_sleep_s": 5.137613,
      "fixed_1_1_12_sleep_s": 0.078078,
      "latest_1_1_13_sleep_s": 0.078999,
      "bypass": false
    },
    "c3_page_render_GET": {
      "entry": "GET /?page_id=<wishlist_page>",
      "sink": "getWishlistsByKeys(cookieGet()) via Frontend::wishlistPage()",
      "vuln_1_1_11_sleep_s": 5.125263,
      "fixed_1_1_12_sleep_s": 0.10013,
      "latest_1_1_13_sleep_s": 0.09568,
      "bypass": false
    }
  },
  "source_only_candidate": {
    "c4_wp_login_update": {
      "entry": "wp_login action -> Frontend::assignUserWishlistFromCookie()",
      "sink": "setUserToWishlistsByKeys(userId, cookieGet()) - UPDATE ... WHERE wishlist_key IN ('...') single-quote breakout",
      "auth_required": true,
      "tested_at_runtime": false,
      "covered_by_fix": true,
      "note": "Authenticated trigger (fires on any login). Different sink (UPDATE) and different quote style (single) from the parent SELECT/double-quote. Covered by cookieGet() regex + $wpdb->prepare() parameterization of setUserToWishlistsByKeys. Time-based oracle is unreliable on an empty wishlist table (UPDATE WHERE scans no rows), so validated by source inspection rather than runtime."
    }
  },
  "artifact_refs": {
    "variant_manifest": "vuln_variant/variant_manifest.json",
    "validation_verdict": "vuln_variant/validation_verdict.json",
    "runtime_manifest": "vuln_variant/runtime_manifest.json",
    "repro_log": "logs/vuln_variant_reproduction.log",
    "root_cause_equivalence": "vuln_variant/root_cause_equivalence.json",
    "reproducer": ["vuln_variant/reproduction_steps.sh"]
  }
}
