Skip to content

Fix vendor product IDORs and consolidate ownership checks in Grand.Web.Vendor - #782

Merged
KrzysztofPajak merged 1 commit into
developfrom
fix/vendor-product-idor
Aug 14, 2026
Merged

Fix vendor product IDORs and consolidate ownership checks in Grand.Web.Vendor#782
KrzysztofPajak merged 1 commit into
developfrom
fix/vendor-product-idor

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: bugfix

Issue

Two IDOR vulnerabilities were found in Grand.Web.Vendor product sub-resource mutations:

  1. BundleProductModel field mismatch (ProductViewModelService.UpdateBundleProductModel/DeleteBundleProductModel): the model carries two ids — ProductBundleId (the parent product actually loaded and mutated) and ProductId (the bundled component). The global IProductValidVendor ownership filter only validates ProductId. A vendor could submit their own product as ProductId to pass validation while mutating another vendor's bundle-product list via ProductBundleId.
  2. ProductRelatedValidVendor OR-bypass: the validator behind IProductRelatedValidVendor accepted ownership of eitherProductId1 or ProductId2, but RelatedProductUpdate/Delete and SimilarProductUpdate/Delete only ever read/mutate ProductId1's mapping list. A vendor owning any product could supply it as ProductId2 to pass validation while editing/deleting another vendor's ProductId1 mapping entry.

Separately, ownership checks across this area were duplicated ad hoc (~50 inline entity.VendorId != CurrentVendor.Id comparisons, plus a private CheckAccessToProduct on ProductController that reimplemented an existing shared helper), which is how issue #1 above stayed unnoticed — the check that matters is easy to miss or misplace when it isn't consolidated.

Solution

  • Added an explicit ownership check on the loaded parent product in UpdateBundleProductModel/DeleteBundleProductModel.
  • Changed ProductRelatedValidVendor to require ownership of ProductId1 only, matching what the consuming actions actually mutate.
  • Consolidated all ownership checks in the Vendor area onto the existing IWorkContext.HasAccessToX(entity) extensions in Extensions/HasAccess.cs (checking the loaded entity right before mutation, not a request DTO field), removing the duplicated inline comparisons and the redundant CheckAccessToProduct. Added HasAccessToVendorReview/HasAccessToMerchandiseReturn for the two controllers that had no shared helper yet.
  • Documented the pattern and the failure mode with XML doc comments on HasAccess.cs and IProductValidVendor.cs so a future model with more than one product-id-shaped field doesn't reintroduce the same bug.

Breaking changes

None. ProductRelatedValidVendor becomes stricter (was accepting an alternative ownership path that was never intentionally needed by any current caller); all other changes are behavior-preserving refactors of duplicated checks onto a single existing helper.

Testing

  1. Check out this branch and run dotnet build src/Web/Grand.Web.Vendor/Grand.Web.Vendor.csproj — builds clean, 0 warnings/errors.
  2. As vendor A, create a bundle-product mapping row id on vendor B's product (e.g. by inspecting the grid's hidden Id/ProductBundleId fields via devtools on a page you have access to, or via direct POST), then POST to BundleProductUpdate/BundleProductDelete with ProductBundleId = vendor B's product and ProductId = one of vendor A's own products. Before this fix: request succeeds and mutates vendor B's product. After: request throws/returns a permission error.
  3. As vendor A, POST to RelatedProductUpdate/RelatedProductDelete (or the Similar-product equivalents) with ProductId1 = vendor B's product (and an existing mapping Id on it) and ProductId2 = one of vendor A's own products. Before: request succeeds. After: request is rejected by ProductRelatedValidVendor.
  4. Regression-check normal vendor flows: editing/adding/removing categories, collections, related/similar/bundle/cross-sell/recommended products, pictures, and spec attributes on a vendor's own products still works as before.

No dedicated Grand.Web.Vendor.Tests project exists yet, so this is verified via manual testing and dotnet build rather than an automated test run.

🤖 Generated with Claude Code

Two confirmed IDOR bugs in Grand.Web.Vendor product sub-resource
mutations, plus a consolidation pass on how vendor ownership is
checked across the area:
- BundleProductModel field mismatch: Update/DeleteBundleProductModel
loaded and mutated the parent product via ProductBundleId, but the
global IProductValidVendor filter only validated ProductId (the
bundled component). A vendor could pass their own product as
ProductId to pass validation while mutating another vendor's
ProductBundleId. Fixed with an explicit ownership check on the
loaded parent.
- ProductRelatedValidVendor OR-bypass: accepted ownership of either
ProductId1 or ProductId2, but RelatedProductUpdate/Delete and
SimilarProductUpdate/Delete only ever mutate ProductId1's mapping
list. An attacker owning any product could supply it as ProductId2
to pass validation while editing someone else's ProductId1. Fixed
by requiring ownership of ProductId1 only.
- Consolidated ~50 inline `entity.VendorId != CurrentVendor.Id`
checks (including a duplicate of HasAccessToProduct living as a
private method on ProductController) onto the existing
IWorkContext.HasAccessToX(entity) extensions in
Extensions/HasAccess.cs, and added HasAccessToVendorReview /
HasAccessToMerchandiseReturn to cover the two controllers that had
no shared helper at all. Checking the loaded entity right before
mutation (instead of a request DTO field) is what makes the first
two bugs structurally impossible to reintroduce.
- Documented the pattern and the failure mode with XML doc comments
on HasAccess.cs and IProductValidVendor.cs.
No test project exists yet for Grand.Web.Vendor; verified via
`dotnet build src/Web/Grand.Web.Vendor/Grand.Web.Vendor.csproj`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 14, 2026 07:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@KrzysztofPajak
KrzysztofPajak merged commit 0196330 into developAug 14, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the fix/vendor-product-idor branch August 14, 2026 15:57
Sign up for freeto 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.

2 participants

@KrzysztofPajak