# CRMEB storefront gift-order endpoints leak buyer data and let any logged-in user take over another user's paid gift order

**Class:** CWE-639 Authorization Bypass Through User-Controlled Key, CWE-200 Exposure of Sensitive Information, CWE-863 Incorrect Authorization
CRMEB exposes a storefront gift-order feature through two API routes, `order/gift_detail/:oid` and `order/receive_gift/:oid`. Both sit behind the login middleware `AuthTokenMiddleware::class, true`, which requires a valid user token but does not check order ownership. The handler `StoreOrderServices::giftDetail($oid)` loads the order by its primary key alone, `getOne(['id' => $oid, 'is_del' => 0])`, with no `uid` filter and no secret-key check. The order primary key is a sequential auto-increment integer. Any self-registered storefront user can enumerate order ids and read another user's buyer nickname, avatar, full product list, total quantity, and paid amount. The same response also returns a computed `gift_key`, which is the only secret that gates claiming a gift order. Because `receiveGift` validates the exact same md5 value, an attacker who reads a victim's order obtains the secret needed to claim that order, write the attacker as `gift_uid`, and overwrite the delivery name, phone, and address with the attacker's own. The result is cross-user data disclosure for every order and takeover of the physical goods for any unclaimed gift order. The feature is reachable under default configuration because open storefront registration is on by default.
The gift-order feature lets a buyer pay for an order and hand it to a different user to claim. The design intent is that only someone holding the order's secret `gift_key` can act on it. The server undermines that intent in two places.
### Route registration and authorization level
Both routes are declared inside a route group that applies only login enforcement, not ownership. The group closes with `AuthTokenMiddleware::class, true`. The `true` flag forces a valid token but establishes only that the caller is some logged-in user.
Route::post('order/receive_gift/:oid', 'v1.order.StoreOrderController/receiveGift')->name('receiveGift')->option(['real_name' => '领取礼物']);//领取礼物
Route::get('order/gift_detail/:oid', 'v1.order.StoreOrderController/giftDetail')->name('giftDetail')->option(['real_name' => '礼品详情']); //礼品详情
The authentication middleware sets `request->uid()` from the bearer token. It does not associate the token with the `:oid` in the path, so the handler is responsible for its own ownership check.
`crmeb/app/api/middleware/AuthTokenMiddleware.php`
// parseToken returns the user, uid() exposes (int)$authInfo['user']->uid or 0
$authInfo = $userServices->parseToken($token);
### The disclosure sink reads by primary key only
The controller passes the path parameter straight through.
`crmeb/app/api/controller/v1/order/StoreOrderController.php:964`
return app('json')->success($this->services->giftDetail($oid));
The service loads the order with no owner scope and no secret check.
`crmeb/app/services/order/StoreOrderServices.php:2998`
$orderInfo = $this->dao->getOne(['id' => $oid, 'is_del' => 0]);
$orderInfo = $this->tidyOrder($orderInfo, true);
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->get($orderInfo['uid']);
'avatar' => set_file_url($userInfo['avatar']),
'gift_key' => md5($orderInfo['id'] . '_' . $orderInfo['order_id'] . '_' . $orderInfo['uid']),
'refund_status' => $orderInfo['refund_status'],
'store_self_mention' => (int)sys_config('store_self_mention') ?? 0,
The lookup key `['id' => $oid]` is the `store_order` table primary key, an auto-increment integer. There is no `uid` condition, no `gift_mark` condition, and no requirement that the caller present the `gift_key`. `tidyOrder($orderInfo, true)` fills `cartInfo` with the ordered product rows (product name, sku, images, price). The response therefore discloses, for any order id, the buyer identity and the full contents of the order.
Sibling order endpoints in the same service show the correct pattern. `order/detail`, `order/refund_detail`, and `order/express` all call owner-scoped lookups such as `getUserOrderByKey($services, $uni, (int)$request->uid())` and `getUserOrderDetail($uni, (int)$request->uid(), ...)`. The gift path is the one that drops the uid scope.
### The returned gift_key is the only secret that gates takeover
The response includes `gift_key => md5($id . '_' . $order_id . '_' . $uid)`. All three inputs are server-side fields of the same order that giftDetail just disclosed. The claim handler validates that exact value and nothing stronger.
`crmeb/app/services/order/StoreOrderServices.php:3043`
public function receiveGift($uid, $oid, $gift_key, $shipping_type, $name, $phone, $address_id = 0, $store_id = 0)
if ($gift_key != md5($orderInfo['id'] . '_' . $orderInfo['order_id'] . '_' . $orderInfo['uid'])) {
if ($orderInfo['gift_uid'] != 0 && $orderInfo['gift_uid'] != $uid) {
if ($shipping_type == 1 && $address_id) {
$addressInfo = app()->make(UserAddressServices::class)->getOne(['uid' => $uid, 'id' => $address_id, 'is_del' => 0]);
$address = $addressInfo['province'] . ' ' . $addressInfo['city'] . ' ' . $addressInfo['district'] . ' ' . $addressInfo['detail'];
The guards after the key check are `refund_status == 0`, `uid != orderInfo.uid`, and `gift_uid == 0`. None of them stops a third party. The `uid != orderInfo.uid` guard only blocks claiming your own order, which helps the attacker rather than the victim. On success the handler writes `gift_uid = attacker uid` and overwrites `real_name`, `user_phone`, and `user_address` from the attacker's own address record. The paid order's delivery is now directed to the attacker.
participant A as Attacker (logged-in storefront user)
A->>M: GET order gift_detail with guessed order id and attacker token
G-->>A: buyer identity, product list, price, and the gift_key
A->>M: POST order receive_gift with same order id and leaked gift_key
R->>DB: set gift_uid to attacker and overwrite delivery to attacker address
R-->>A: claim success, goods ship to attacker
Prerequisites. A CRMEB deployment with default open storefront registration. One attacker storefront account with a valid bearer token. The attacker needs no relationship to the victim and no prior knowledge of any gift_key. Order primary keys are sequential integers, so valid ids are guessed by counting up or down from the attacker's own order id. The request examples below are illustrative and were not executed against any live system.
1. Register or log in as an ordinary storefront user and obtain the bearer token. State before: attacker holds a normal low-privilege session.
Response: a JSON body containing the token used as `Authori-zation: Bearer `.
2. Read a target order by guessing its id. State change: none on the server, this is a read.
GET /api/order/gift_detail/10231 HTTP/1.1
Response (illustrative): a JSON success body containing `uid`, `nickname`, `avatar`, `cartInfo` with the product rows, `pay_price`, `total_num`, `gift_mark`, `gift_uid`, `refund_status`, and `gift_key`. The attacker now holds the victim's buyer identity, the full product list, the amount paid, and the order's secret `gift_key`.
3. Confirm the order is claimable. From the step 2 body, check `refund_status == 0` and `gift_uid == 0`. If both hold the order is an unclaimed gift order and can be taken over.
4. Claim the order using the leaked gift_key and the attacker's own address. State change: the server sets `gift_uid` to the attacker uid and overwrites the delivery `real_name`, `user_phone`, and `user_address` with the attacker's address record.
POST /api/order/receive_gift/10231 HTTP/1.1
{"gift_key":" ","shipping_type":1,"address_id": }
Response (illustrative): a JSON success body with `{"status":1}` meaning the gift was claimed. The paid goods are now scheduled for delivery to the attacker's address.
5. Repeat step 2 across a range of order ids to harvest buyer records at scale, independent of whether any given order is a claimable gift order.
Runtime validation was not performed. No request was executed against a live CRMEB deployment, no account or PoC was run, and no database was modified. The finding was derived from static review of the pinned released source read through GitHub raw source pages.
- Route group with login-only enforcement: `crmeb/app/api/route/v1.php:234-235`, group closing middleware `AuthTokenMiddleware::class, true` at `crmeb/app/api/route/v1.php:376`.
- Controller entry: `crmeb/app/api/controller/v1/order/StoreOrderController.php:964` (`giftDetail`) and `:972` (`receiveGift`).
- Unscoped lookup and gift_key disclosure: `crmeb/app/services/order/StoreOrderServices.php:2998-3041`, specifically the `getOne(['id' => $oid, 'is_del' => 0])` on line 3000 and the `gift_key` md5 on line 3034.
- Claim validation and delivery overwrite: `crmeb/app/services/order/StoreOrderServices.php:3043-3085`, specifically the md5 comparison on line 3049 and the delivery write on lines 3074-3083.
- Owner-scoped sibling handlers for contrast: `order/detail`, `order/refund_detail`, `order/express` call `getUserOrderByKey`/`getUserOrderDetail` with `(int)$request->uid()`.
The same unscoped `getOne(['id' => $oid, 'is_del' => 0])` and the same route are present at tag v5.6.1 (`crmeb/app/services/order/StoreOrderServices.php:2990`, route `crmeb/app/api/route/v1.php:211`), which supports the affected range v5.6.1 through v6.0.0.
Not established by this review: actual HTTP status codes and JSON bodies from a running instance, and any environment where a reverse proxy or front-end layer blocks the route.
Disclosure. Any authenticated storefront user can read any order by guessing its sequential id through `order/gift_detail/:oid`. The response discloses the buyer uid, nickname, avatar, the full ordered product list, total quantity, paid amount, and the order's `gift_key`. This applies to every order regardless of gift state, so mass harvesting of buyer records and order contents is possible with a single low-privilege account.
Takeover. For any order that is an unclaimed gift order (`gift_uid == 0`) and not refunded (`refund_status == 0`), the attacker uses the leaked `gift_key` with `order/receive_gift/:oid` to set themselves as the recipient and overwrite the delivery name, phone, and address with their own. The physical goods paid for by the victim are redirected to the attacker, and the attacker's claim blocks the intended recipient.
What is NOT established. No remote code execution. No SQL injection. No administrator compromise. No server-side file access. Takeover requires the target order to be in the unclaimed non-refunded gift state, while disclosure has no such precondition. The attacker cannot read the victim's stored address book through this path, only the order record and buyer profile fields returned by the handler.
**Where exactly:** `crmeb/app/services/order/StoreOrderServices.php:2998-3041` (`giftDetail`) and `crmeb/app/services/order/StoreOrderServices.php:3043-3049` (`receiveGift` gate).
+ public function giftDetail($oid, string $gift_key = '')
$orderInfo = $this->dao->getOne(['id' => $oid, 'is_del' => 0]);
+ // the caller must present the order's secret key, never hand it out unconditionally
+ if ($gift_key !== md5($orderInfo['id'] . '_' . $orderInfo['order_id'] . '_' . $orderInfo['uid'])) {
+ throw new ApiException('无权查看该订单');
+ // this is a gift , refuse non-gift orders
+ if ((int)$orderInfo['gift_mark'] !== 1) {
+ throw new ApiException('订单不存在');
$orderInfo = $this->tidyOrder($orderInfo, true);
'avatar' => set_file_url($userInfo['avatar']),
- 'gift_key' => md5($orderInfo['id'] . '_' . $orderInfo['order_id'] . '_' . $orderInfo['uid']),
'refund_status' => $orderInfo['refund_status'],
'store_self_mention' => (int)sys_config('store_self_mention') ?? 0,
Short term. Require the caller of `giftDetail` to present the correct `gift_key` before any order data is returned, and stop returning the `gift_key` in the response body. A link should carry the key, the server should never mint it for an arbitrary order id on request.
Medium term. Replace the derivable md5 key with a random high-entropy token stored on the order row at gift-creation time, so the secret cannot be recomputed from `id`, `order_id`, and `uid`. Scope every gift lookup by `gift_mark = 1` and by the stored token.
Defense in depth. Return the buyer identity and product list only to the order owner or to a caller holding the stored token. Avoid exposing sequential order primary keys in client-facing routes, prefer the opaque `order_id` string. Add server-side rate limiting on `order/gift_detail/:oid` to blunt enumeration.
The core fix is to gate `giftDetail` on a presented secret and remove the secret from the response, then harden the secret to a stored random token scoped to gift orders. That closes both the cross-user disclosure and the downstream takeover.
- No public advisory for this gift-order chain at time of writing.
- Not a duplicate of CVE-2026-1733, which is the integral/points-mall endpoint `/api/store_integral/order/detail/:uni`, a different route and handler.
- Pinned source, giftDetail and receiveGift:
- Pinned source, controller entry:
- Pinned source, route registration:
- Pinned source, auth middleware:
- Earlier affected release:
Reported by hieuPenguinnn, [github.com/hieuPenguinnn](
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
