Skip to content

Fix IDOR on client /orders/view/{order} routes#14

Open
LaAlexita wants to merge 2 commits into
wemxnet:mainfrom
LaAlexita:fix/orders-idor-authorization
Open

Fix IDOR on client /orders/view/{order} routes#14
LaAlexita wants to merge 2 commits into
wemxnet:mainfrom
LaAlexita:fix/orders-idor-authorization

Conversation

@LaAlexita
Copy link
Copy Markdown

Summary

The Client\OrdersController was accepting route-bound Order models on view, payments, emails, members, subscription, and subscribe without checking that the authenticated user actually owns the order (or is an accepted member of it). Any logged-in customer could change the {order:id} segment in the URL and read another customer's order data.

Concretely, an authenticated attacker could browse:

  • /orders/view/<foreign_order_id> — package, billing cycle, status, due date, last renewal
  • /orders/view/<foreign_order_id>/payments — paid payment descriptions, amounts, gateways, transaction IDs, dates
  • /orders/view/<foreign_order_id>/emails — email subjects, from/to, status (the email body link is gated, but the metadata isn't)
  • /orders/view/<foreign_order_id>/members — invited member emails and status
  • /orders/view/<foreign_order_id>/subscription — subscription IDs, gateways, billing dates
  • /orders/view/<foreign_order_id>/subscription/subscribe/<gateway_id> — creates a Subscription row pointing at someone else's order

CWE-639 (Authorization Bypass Through User-Controlled Key) / CWE-200.

Fix

Add a private authorizeOrderAccess(Order $order) helper to Client\OrdersController and call it at the top of every method that renders order-bound content. The helper aborts with 403 unless one of:

  1. $order->user_id === auth()->id() — the requester owns the order, or
  2. there is an OrderMember row for this order with status = 'active' and user_id = auth()->id() — the requester accepted an invitation.

Condition (2) mirrors how OrderActions::acceptInviteAsClient() activates a member (sets both status='active' and user_id), so accepted members keep their existing access.

Test plan

Reproduced against a running install with two accounts (OWNER owns order #2, ATTACKER has no relation to it).

Before the fix, with ATTACKER's session cookie:

/orders/view/2              -> 200
/orders/view/2/payments     -> 200
/orders/view/2/emails       -> 200
/orders/view/2/members      -> 200
/orders/view/2/subscription -> 200

After the fix (same cookie, same order):

/orders/view/2              -> 403
/orders/view/2/payments     -> 403
/orders/view/2/emails       -> 403
/orders/view/2/members      -> 403
/orders/view/2/subscription -> 403

Owner access (OWNER's cookie on order #2) and accepted-member access continue to work — the helper short-circuits on the user_id match or the active-member query.

  • Verify owner can still see their own order tabs
  • Verify accepted members can still see the order they were invited to
  • Verify foreign users get 403 on every tab

The Client\OrdersController accepted route-bound Order models on
view/payments/emails/members/subscription/subscribe without checking
that the authenticated user is the order owner (or an accepted active
member). Any logged-in customer could change the {order:id} segment in
the URL and read another customer's package, status, due date,
payments, invited members, subscription metadata, and provider
transaction IDs.

Add a private authorizeOrderAccess() helper that aborts 403 unless
the user is the order owner or an active OrderMember (status=active
with user_id matching the current user, which is how invites are
linked once accepted in OrderActions::acceptInviteAsClient).

Call it at the top of every method that renders order-bound content.
Copilot AI review requested due to automatic review settings May 23, 2026 10:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses an IDOR vulnerability in the client-facing order routes by ensuring the authenticated user is authorized to access a route-bound Order (either as the order owner or an active order member) before rendering order-related pages or creating a subscription.

Changes:

  • Added a controller-level authorizeOrderAccess(Order $order) authorization helper.
  • Invoked the authorization helper at the start of all order-bound client actions (view, payments, emails, members, subscription, subscribe).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +86
private function authorizeOrderAccess(Order $order): void
{
if ($order->user_id === auth()->id()) {
return;
}

$isActiveMember = $order->members()
->where('status', 'active')
->where('user_id', auth()->id())
->exists();

abort_unless($isActiveMember, 403);
}
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.

2 participants