GHSA-665X-PPC4-685W

Vulnerability from github – Published: 2026-04-21 15:20 – Updated: 2026-04-27 16:31
VLAI
Summary
OpenMage LTS: Cross-user wishlist import leads to private option & file disclosure
Details

Cross-user wishlist item import via shared wishlist code, leading to private option disclosure and file-disclosure variant

Summary

The shared wishlist add-to-cart endpoint authorizes access with a public sharing_code, but loads the acted-on wishlist item by a separate global wishlist_item_id and never verifies that the item belongs to the shared wishlist referenced by that code.

This lets an attacker use:

  • a valid shared wishlist code for wishlist A
  • a wishlist item ID belonging to victim wishlist B

to import victim item B into the attacker's cart through the shared wishlist flow for wishlist A.

Because the victim item's stored buyRequest is reused during cart import, the victim's private custom-option data is copied into the attacker's quote. If the product uses a file custom option, this can be elevated to cross-user file disclosure because the imported file metadata is preserved and the download endpoint is not ownership-bound.

Vulnerability Type

  • Broken object-level authorization / IDOR
  • Cross-user data disclosure
  • Cross-user file disclosure variant

Root Cause

In app/code/core/Mage/Wishlist/controllers/SharedController.php, the shared flow does:

$item = Mage::getModel('wishlist/item')->load($itemId);
$wishlist = Mage::getModel('wishlist/wishlist')->loadByCode($code);
...
$item->addToCart($cart);

Relevant lines:

  • SharedController.php:86 loads the wishlist item by global ID
  • SharedController.php:87 loads the wishlist by shared code
  • SharedController.php:99 imports the item into cart

There is no check that:

$item->getWishlistId() == $wishlist->getId()

The safe owner flow in app/code/core/Mage/Wishlist/controllers/IndexController.php:521-528 does preserve this binding by deriving the wishlist from item->getWishlistId().

The imported item keeps its original buyRequest because app/code/core/Mage/Wishlist/Model/Item.php:370-372 passes that stored request directly into:

$cart->addProduct($product, $buyRequest);

Security Impact

Baseline impact

An attacker can import another user's private wishlist item into the attacker's own cart, using an unrelated shared wishlist code.

This is a clear cross-user authorization bypass. The victim item's private configuration is copied into the attacker's quote, including custom-option values such as personalized text.

Stronger variant: cross-user file disclosure

If the victim item contains a custom option of type file, the imported quote item preserves file metadata such as:

  • quote_path
  • order_path
  • secret_key

The file option renderer in app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php:547-552 generates a download URL from:

  • the imported sales/quote_item_option ID
  • the preserved secret_key

The downloader in app/code/core/Mage/Sales/controllers/DownloadController.php:150-185:

  • loads quote item option by global ID
  • verifies only product option type and secret_key
  • reads the file from order_path or quote_path

It does not verify ownership of the quote item, order, or original wishlist item. This creates a cross-user file disclosure path once victim file metadata has been imported.

Steps To Reproduce

Lab data

  • shared wishlist A:
  • wishlist_id = 1
  • customer_id = 2
  • sharing_code = 6376bb8c37a09c2de3664bd8cdc16412
  • victim wishlist B:
  • wishlist_id = 2
  • customer_id = 3
  • victim item:
  • wishlist_item_id = 1
  • wishlist_id = 2
  • product_id = 2
  • victim private text option marker:
  • VICTIM-MARKER-49040822

Reproduction

Send:

GET /wishlist/shared/cart/?code=6376bb8c37a09c2de3664bd8cdc16412&item=1

Where:

  • code belongs to shared wishlist A
  • item=1 belongs to victim wishlist B

Expected result

The request should be rejected because the item does not belong to the shared wishlist referenced by the sharing_code.

Actual result

The application imports victim item 1 into the attacker's quote anyway.

Verified Evidence

Baseline variant

Previously verified at quote/option level in lab:

option_1 = VICTIM-MARKER-49040822

This shows that the attacker's cart received victim-private custom-option data from another user's wishlist item.

File-disclosure variant

Previously verified in lab after importing a victim file-option payload:

/sales/download/downloadCustomOption/id/9/key/86fca9b61c0b891b52fb/

This URL was generated from imported quote item option data containing the victim file metadata and secret key.

Why This Is A Valid Bug

This is not a timing issue and does not depend on non-default security settings.

The bug is a direct authorization failure:

  • authorization is based on wishlist A's share code
  • the acted-on object is item B from another wishlist
  • there is no item-to-wishlist binding check
  • victim-controlled item state is then copied into attacker-controlled cart state

That is a broken object-level authorization issue with clear cross-user impact.

Remediation

In SharedController::cartAction(), reject any request where the loaded item does not belong to the wishlist loaded from the share code:

$item = Mage::getModel('wishlist/item')->load($itemId);
$wishlist = Mage::getModel('wishlist/wishlist')->loadByCode($code);

if (!$item->getId() || !$wishlist->getId() || (int) $item->getWishlistId() !== (int) $wishlist->getId()) {
    return $this->_forward('noRoute');
}

Defense in depth:

  • bind sales/download/downloadCustomOption to the current quote/order owner instead of trusting only id + secret_key
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "openmage/magento-lts"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "20.17.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40098"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-21T15:20:41Z",
    "nvd_published_at": "2026-04-20T17:16:34Z",
    "severity": "MODERATE"
  },
  "details": "# Cross-user wishlist item import via shared wishlist code, leading to private option disclosure and file-disclosure variant\n\n## Summary\n\nThe shared wishlist add-to-cart endpoint authorizes access with a public `sharing_code`, but loads the acted-on wishlist item by a separate global `wishlist_item_id` and never verifies that the item belongs to the shared wishlist referenced by that code.\n\nThis lets an attacker use:\n\n- a valid shared wishlist code for wishlist A\n- a wishlist item ID belonging to victim wishlist B\n\nto import victim item B into the attacker\u0027s cart through the shared wishlist flow for wishlist A.\n\nBecause the victim item\u0027s stored `buyRequest` is reused during cart import, the victim\u0027s private custom-option data is copied into the attacker\u0027s quote. If the product uses a file custom option, this can be elevated to cross-user file disclosure because the imported file metadata is preserved and the download endpoint is not ownership-bound.\n\n## Vulnerability Type\n\n- Broken object-level authorization / IDOR\n- Cross-user data disclosure\n- Cross-user file disclosure variant\n\n## Root Cause\n\nIn `app/code/core/Mage/Wishlist/controllers/SharedController.php`, the shared flow does:\n\n```php\n$item = Mage::getModel(\u0027wishlist/item\u0027)-\u003eload($itemId);\n$wishlist = Mage::getModel(\u0027wishlist/wishlist\u0027)-\u003eloadByCode($code);\n...\n$item-\u003eaddToCart($cart);\n```\n\nRelevant lines:\n\n- `SharedController.php:86` loads the wishlist item by global ID\n- `SharedController.php:87` loads the wishlist by shared code\n- `SharedController.php:99` imports the item into cart\n\nThere is no check that:\n\n```php\n$item-\u003egetWishlistId() == $wishlist-\u003egetId()\n```\n\nThe safe owner flow in `app/code/core/Mage/Wishlist/controllers/IndexController.php:521-528` does preserve this binding by deriving the wishlist from `item-\u003egetWishlistId()`.\n\nThe imported item keeps its original `buyRequest` because `app/code/core/Mage/Wishlist/Model/Item.php:370-372` passes that stored request directly into:\n\n```php\n$cart-\u003eaddProduct($product, $buyRequest);\n```\n\n## Security Impact\n\n### Baseline impact\n\nAn attacker can import another user\u0027s private wishlist item into the attacker\u0027s own cart, using an unrelated shared wishlist code.\n\nThis is a clear cross-user authorization bypass. The victim item\u0027s private configuration is copied into the attacker\u0027s quote, including custom-option values such as personalized text.\n\n### Stronger variant: cross-user file disclosure\n\nIf the victim item contains a custom option of type `file`, the imported quote item preserves file metadata such as:\n\n- `quote_path`\n- `order_path`\n- `secret_key`\n\nThe file option renderer in `app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php:547-552` generates a download URL from:\n\n- the imported `sales/quote_item_option` ID\n- the preserved `secret_key`\n\nThe downloader in `app/code/core/Mage/Sales/controllers/DownloadController.php:150-185`:\n\n- loads quote item option by global ID\n- verifies only product option type and `secret_key`\n- reads the file from `order_path` or `quote_path`\n\nIt does not verify ownership of the quote item, order, or original wishlist item. This creates a cross-user file disclosure path once victim file metadata has been imported.\n\n## Steps To Reproduce\n\n### Lab data\n\n- shared wishlist A:\n  - `wishlist_id = 1`\n  - `customer_id = 2`\n  - `sharing_code = 6376bb8c37a09c2de3664bd8cdc16412`\n- victim wishlist B:\n  - `wishlist_id = 2`\n  - `customer_id = 3`\n- victim item:\n  - `wishlist_item_id = 1`\n  - `wishlist_id = 2`\n  - `product_id = 2`\n- victim private text option marker:\n  - `VICTIM-MARKER-49040822`\n\n### Reproduction\n\nSend:\n\n```http\nGET /wishlist/shared/cart/?code=6376bb8c37a09c2de3664bd8cdc16412\u0026item=1\n```\n\nWhere:\n\n- `code` belongs to shared wishlist A\n- `item=1` belongs to victim wishlist B\n\n### Expected result\n\nThe request should be rejected because the item does not belong to the shared wishlist referenced by the `sharing_code`.\n\n### Actual result\n\nThe application imports victim item `1` into the attacker\u0027s quote anyway.\n\n## Verified Evidence\n\n### Baseline variant\n\nPreviously verified at quote/option level in lab:\n\n```text\noption_1 = VICTIM-MARKER-49040822\n```\n\nThis shows that the attacker\u0027s cart received victim-private custom-option data from another user\u0027s wishlist item.\n\n### File-disclosure variant\n\nPreviously verified in lab after importing a victim file-option payload:\n\n```text\n/sales/download/downloadCustomOption/id/9/key/86fca9b61c0b891b52fb/\n```\n\nThis URL was generated from imported quote item option data containing the victim file metadata and secret key.\n\n## Why This Is A Valid Bug\n\nThis is not a timing issue and does not depend on non-default security settings.\n\nThe bug is a direct authorization failure:\n\n- authorization is based on wishlist A\u0027s share code\n- the acted-on object is item B from another wishlist\n- there is no item-to-wishlist binding check\n- victim-controlled item state is then copied into attacker-controlled cart state\n\nThat is a broken object-level authorization issue with clear cross-user impact.\n\n## Remediation\n\nIn `SharedController::cartAction()`, reject any request where the loaded item does not belong to the wishlist loaded from the share code:\n\n```php\n$item = Mage::getModel(\u0027wishlist/item\u0027)-\u003eload($itemId);\n$wishlist = Mage::getModel(\u0027wishlist/wishlist\u0027)-\u003eloadByCode($code);\n\nif (!$item-\u003egetId() || !$wishlist-\u003egetId() || (int) $item-\u003egetWishlistId() !== (int) $wishlist-\u003egetId()) {\n    return $this-\u003e_forward(\u0027noRoute\u0027);\n}\n```\n\nDefense in depth:\n\n- bind `sales/download/downloadCustomOption` to the current quote/order owner instead of trusting only `id + secret_key`",
  "id": "GHSA-665x-ppc4-685w",
  "modified": "2026-04-27T16:31:43Z",
  "published": "2026-04-21T15:20:41Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/OpenMage/magento-lts/security/advisories/GHSA-665x-ppc4-685w"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40098"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OpenMage/magento-lts/pull/5446"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/OpenMage/magento-lts"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OpenMage/magento-lts/releases/tag/v20.17.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "OpenMage LTS: Cross-user wishlist import leads to private option \u0026 file disclosure"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…