Skip to content

fix(security): prevent cross-vendor download-permission granting in grant_access_to_download (IDOR)#3293

Open
MdAsifHossainNadim wants to merge 1 commit into
developfrom
fix/grant-download-ownership-idor
Open

fix(security): prevent cross-vendor download-permission granting in grant_access_to_download (IDOR)#3293
MdAsifHossainNadim wants to merge 1 commit into
developfrom
fix/grant-download-ownership-idor

Conversation

@MdAsifHossainNadim

@MdAsifHossainNadim MdAsifHossainNadim commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

All Submissions:

  • My code follows the WordPress coding standards
  • My code is tested
  • My code has proper inline documentation

Changes proposed in this Pull Request:

Fixes a cross-vendor IDOR in the "grant download access" AJAX handler: a vendor could grant a customer download permissions for another vendor's downloadable files.

What's the actual issue?

grant_access_to_download() (wp_ajax_dokan_grant_access_to_download) verifies the nonce, the dokandar capability, and dokan_is_seller_has_order( current_user, order_id ) — i.e. it confirms the vendor owns the order. It then loops over the attacker-controlled $_POST['product_ids'] and calls wc_downloadable_file_permission() for each, without checking that the vendor owns those products:

foreach ( $product_ids as $product_id ) {
    $product = dokan()->product->get( $product_id );
    $files   = $product->get_downloads();           // <-- any product id, including another vendor's
    ...
    wc_downloadable_file_permission( $download_id, $product_id, $order );
}

So a vendor could send another vendor's downloadable product id and grant their own customer free, working download permissions for that vendor's paid files. (The unchecked $product also fataled on an invalid id.)

How we fixed itincludes/Ajax.php

Skip any product the current vendor does not own before granting:

foreach ( $product_ids as $product_id ) {
    $product = dokan()->product->get( $product_id );

    // Only grant downloads for the vendor's own products, never another vendor's files.
    if ( ! $product || ! dokan_is_product_author( $product_id ) ) {
        continue;
    }

    $files = $product->get_downloads();
    ...
}

dokan_is_product_author() is Dokan's canonical product-ownership primitive (vendor-staff aware); the same ! $product guard also removes the pre-existing null-dereference on invalid ids.

How to test

  1. As a vendor with a downloadable product in one of your orders, open that order and use Grant access → your own product's download permission is still granted (no regression).
  2. As that vendor, replay the request but swap in another vendor's downloadable product id in product_ids (action dokan_grant_access_to_download, valid grant-access nonce, your own order_id).
    • Before: a woocommerce_downloadable_product_permissions row is created for the foreign product — the customer can download another vendor's file.
    • After: the foreign product is skipped — no permission row is created.

Test result: ✅ Verified on a live install by replaying the handler's exact (patched) grant loop as a non-admin vendor against one of the vendor's own orders: the vendor's own product took the grant path, while a foreign vendor's downloadable product was skipped and zero download-permission rows were created for it. (This is a legacy admin-ajax handler whose UI only renders for orders that contain the vendor's own downloadable products, so it was exercised through the server-side handler path rather than the browser.)

Related Pull Request(s)

  • Security audit tracking (finding L4): getdokan/plugin-internal-tasks#1994

Closes

  • Closes getdokan/plugin-internal-tasks#2002

Changelog entry

Fix — Cross-vendor download-permission granting

The "grant download access" handler only checked order ownership, so a vendor could grant download permissions for another vendor's downloadable products by passing foreign product ids. Each product is now checked for ownership before access is granted.

…(IDOR)

The grant_access_to_download AJAX handler verified order ownership but then
looped attacker-controlled product_ids and granted download permissions for any
product, so a vendor could grant their customer download access to another
vendor's downloadable files. Skip products the current vendor does not own
(dokan_is_product_author); the added null guard also removes a latent
null-dereference on invalid product ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@MdAsifHossainNadim, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53351cfd-cf60-430d-8a0e-cb3b0cabd559

📥 Commits

Reviewing files that changed from the base of the PR and between 5f73436 and 6a3d29a.

📒 Files selected for processing (1)
  • includes/Ajax.php
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/grant-download-ownership-idor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant