Predefined-kit checkout in Snipe-IT does not enforce Full Multiple Company Support (FMCS) tenant isolation on the checkout target. A multi-company non-superuser with assets.checkout permission can route a company-A asset through a predefined kit to a company-B-only user, even though the same actor / asset / target combination is correctly rejected on every other checkout sink (single, bulk, API, accessory, license, consumable).
Severity
Medium. CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N (5.4).
CWE
CWE-863: Incorrect Authorization.
Affected versions
>= 8.6.3, < <PATCHED_VERSION>. Runtime-verified on v8.6.3, code-inspected on v8.7.1. The PredefinedKitCheckoutService was created in 2019 and has never gained a company-isolation check, so earlier versions running with FMCS enabled almost certainly have the same gap, but only the range explicitly verified against a runtime is claimed here.
Description
With FMCS on (Setting::getSettings()->full_multiple_companies_support = 1), every checkout sink Snipe-IT exposes gates the target user against the item's company via $item->canCheckoutTo($target) (defined on App\Models\Traits\CompanyableTrait):
- Single-asset checkout:
AssetCheckoutController::store at app/Http/Controllers/Assets/AssetCheckoutController.php:130 emits general.error_checkout_company_mismatch on failure.
- Bulk asset checkout, API asset checkout, accessory checkout, consumable checkout, license checkout: all call
canCheckoutTo before persisting.
App\Services\PredefinedKitCheckoutService did not. getAssetsToAdd, getLicenseSeatsToAdd, getConsumablesToAdd, and getAccessoriesToAdd each ran only the actor-vs-item policy check ($this->authorize('checkout', $item)) and availableForCheckout(). saveToDb then called $asset->checkOut($user, ...) directly.
Result: an actor who belongs to more than one company can locate a kit whose model has an available asset in company A, POST to /kits/{kit}/checkout with user_id set to a user who belongs only to company B, and the kit-checkout path will assign the company-A asset to the company-B user without triggering the mismatch error. The same actor calling POST /hardware/{asset}/checkout with the same asset and target is blocked.
Actor user-lookup is company-scoped elsewhere, so a single-company actor cannot User::find() a cross-company victim in the first place. This bypass therefore only applies to actors who are members of BOTH the source and destination companies, which is a common MSP / multi-company admin shape.
The reported impact is cross-tenant asset assignment, not remote code execution or authentication bypass. The security invariant that FMCS exists to guarantee (an item belonging to company X cannot be checked out to a user who does not belong to company X or an allowed hierarchy relationship) is violated on the kit path specifically.
Preconditions
full_multiple_companies_support = 1.
- Actor is not a superuser.
- Actor is a member of at least two companies.
- Actor has
assets.checkout permission.
- A predefined kit exists whose model has an available asset in company A, and the checkout target user belongs only to company B.
Reproduction
Verified against snipe/snipe-it:v8.6.3 with MariaDB and FMCS enabled. Actor in companies A + B, super = 0. Asset X in company A. victimB in company B only. allyA in company A. Kit contains that asset's model.
- As actor,
POST /hardware/{X}/checkout with target victimB → blocked. Flash shows general.error_checkout_company_mismatch. assets.assigned_to remains NULL.
POST /kits/{kit}/checkout with user_id = victimB → success. assets.assigned_to set to victimB.
- Same kit checkout to
allyA succeeds (same-company, expected control).
Remediation
App\Services\PredefinedKitCheckoutService now calls $item->canCheckoutTo($user) for every asset candidate, license, consumable, and accessory in the kit before adding it to the batch that reaches the DB. For assets (where a kit model can have many candidate assets) cross-company candidates are skipped and, if the model can no longer fulfill its quantity from same-company candidates, the operator sees the mismatch error naming the specific blocked asset rather than the generic "none available" one. For the single-item item types (license, consumable, accessory) the mismatch error fires immediately.
Regression tests under tests/Feature/PredefinedKits/Ui/CheckoutKitFmcsTest.php cover all four item types against a multi-company non-superuser targeting a wrong-company user and assert that the target row / pivot is not written.
Fix commit
f590af4c87b8086a160c8997cd3cd8787b757d29
Predefined-kit checkout in Snipe-IT does not enforce Full Multiple Company Support (FMCS) tenant isolation on the checkout target. A multi-company non-superuser with
assets.checkoutpermission can route a company-A asset through a predefined kit to a company-B-only user, even though the same actor / asset / target combination is correctly rejected on every other checkout sink (single, bulk, API, accessory, license, consumable).Severity
Medium. CVSS 3.1:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N(5.4).CWE
CWE-863: Incorrect Authorization.
Affected versions
>= 8.6.3, < <PATCHED_VERSION>. Runtime-verified onv8.6.3, code-inspected onv8.7.1. ThePredefinedKitCheckoutServicewas created in 2019 and has never gained a company-isolation check, so earlier versions running with FMCS enabled almost certainly have the same gap, but only the range explicitly verified against a runtime is claimed here.Description
With FMCS on (
Setting::getSettings()->full_multiple_companies_support = 1), every checkout sink Snipe-IT exposes gates the target user against the item's company via$item->canCheckoutTo($target)(defined onApp\Models\Traits\CompanyableTrait):AssetCheckoutController::storeatapp/Http/Controllers/Assets/AssetCheckoutController.php:130emitsgeneral.error_checkout_company_mismatchon failure.canCheckoutTobefore persisting.App\Services\PredefinedKitCheckoutServicedid not.getAssetsToAdd,getLicenseSeatsToAdd,getConsumablesToAdd, andgetAccessoriesToAddeach ran only the actor-vs-item policy check ($this->authorize('checkout', $item)) andavailableForCheckout().saveToDbthen called$asset->checkOut($user, ...)directly.Result: an actor who belongs to more than one company can locate a kit whose model has an available asset in company A, POST to
/kits/{kit}/checkoutwithuser_idset to a user who belongs only to company B, and the kit-checkout path will assign the company-A asset to the company-B user without triggering the mismatch error. The same actor callingPOST /hardware/{asset}/checkoutwith the same asset and target is blocked.Actor user-lookup is company-scoped elsewhere, so a single-company actor cannot
User::find()a cross-company victim in the first place. This bypass therefore only applies to actors who are members of BOTH the source and destination companies, which is a common MSP / multi-company admin shape.The reported impact is cross-tenant asset assignment, not remote code execution or authentication bypass. The security invariant that FMCS exists to guarantee (an item belonging to company X cannot be checked out to a user who does not belong to company X or an allowed hierarchy relationship) is violated on the kit path specifically.
Preconditions
full_multiple_companies_support = 1.assets.checkoutpermission.Reproduction
Verified against
snipe/snipe-it:v8.6.3with MariaDB and FMCS enabled. Actor in companies A + B,super = 0. Asset X in company A.victimBin company B only.allyAin company A. Kit contains that asset's model.POST /hardware/{X}/checkoutwith targetvictimB→ blocked. Flash showsgeneral.error_checkout_company_mismatch.assets.assigned_toremains NULL.POST /kits/{kit}/checkoutwithuser_id = victimB→ success.assets.assigned_toset tovictimB.allyAsucceeds (same-company, expected control).Remediation
App\Services\PredefinedKitCheckoutServicenow calls$item->canCheckoutTo($user)for every asset candidate, license, consumable, and accessory in the kit before adding it to the batch that reaches the DB. For assets (where a kit model can have many candidate assets) cross-company candidates are skipped and, if the model can no longer fulfill its quantity from same-company candidates, the operator sees the mismatch error naming the specific blocked asset rather than the generic "none available" one. For the single-item item types (license, consumable, accessory) the mismatch error fires immediately.Regression tests under
tests/Feature/PredefinedKits/Ui/CheckoutKitFmcsTest.phpcover all four item types against a multi-company non-superuser targeting a wrong-company user and assert that the target row / pivot is not written.Fix commit
f590af4c87b8086a160c8997cd3cd8787b757d29