Snipe-IT Accessory API Company ID Mass Assignment
CVE-2026-54329: A mass assignment flaw in Snipe-IT's Accessories API lets low-privileged users write records across company boundaries, scoring CVSS 8.5.
Overview
CVE-2026-54329 is a mass assignment vulnerability in Snipe-IT, a widely deployed open-source IT asset and license management platform. The flaw lives in the Accessories API store (create) endpoint, which passes raw HTTP request parameters directly into an Eloquent model without filtering out security-sensitive fields. Specifically, company_id — the field that governs which organisational tenant an accessory record belongs to — is included in the model’s $fillable array, making it freely settable by any authenticated caller.
The vulnerability becomes critical when Snipe-IT is configured with Full Multiple Companies Support enabled. In that operational mode, the platform is expected to enforce strict data isolation between tenants. A low-privileged user authenticated under Company A can simply include "company_id": <target_id> in a POST request to /api/v1/accessories and create records that appear to belong to Company B. There is no server-side ownership check that rebinds or rejects the supplied identifier.
Security researchers identified the issue and it was remediated by the Snipe-IT maintainers in version 8.6.2, with three separate commits addressing the mass assignment surface across related model interactions. Any Snipe-IT deployment prior to 8.6.2 running multi-company mode should be treated as actively exploitable by any API-capable user account.
Technical Analysis
The root cause is a textbook CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes — more commonly known as mass assignment. Laravel’s Eloquent ORM provides a fill() / create() mechanism that bulk-assigns request data to model attributes. The protection mechanism is the $fillable whitelist (or inversely, $guarded blacklist) on each model. When a sensitive field like company_id is left in $fillable without a corresponding authorization gate, any caller who can reach the endpoint can manipulate that field.
A simplified representation of the vulnerable pattern inside the AccessoriesController@store method looked like this:
// VULNERABLE — pre-8.6.2
public function store(Request $request)
{
$this->authorize('create', Accessory::class);
$accessory = new Accessory();
$accessory->fill($request->all()); // company_id flows in unchecked
$accessory->user_id = Auth::id();
if ($accessory->save()) {
return response()->json(
Helper::formatStandardApiResponse('success', $accessory, trans('admin/accessories/message.create.success'))
);
}
return response()->json(
Helper::formatStandardApiResponse('error', null, $accessory->getErrors())
);
}
And the Accessory model’s fillable declaration included company_id without restriction:
// VULNERABLE — Accessory model
protected $fillable = [
'category_id',
'company_id', // ← no guard; attacker-controlled
'location_id',
'manufacturer_id',
'min_amt',
'name',
'order_number',
'purchase_cost',
'purchase_date',
'qty',
'supplier_id',
'image',
'notes',
];
The authorization check ($this->authorize('create', Accessory::class)) validates only that the user may create accessories in general — it does not validate whether the company_id in the payload corresponds to a company the authenticated user belongs to. Laravel policies in this codebase were scoped to the user’s own company context at the query level (scoping what records a user can read), but that scope was never enforced at the write level for the company_id field itself.
An attacker crafting a minimal exploit would issue:
POST /api/v1/accessories HTTP/1.1
Host: snipeit.example.internal
Authorization: Bearer <low_priv_token>
Content-Type: application/json
{
"name": "Injected Accessory",
"category_id": 3,
"qty": 1,
"company_id": 7
}
Where 7 is the ID of a foreign company. The record is persisted with company_id = 7 with no further validation. The asset then appears in Company 7’s inventory.
Impact
The practical blast radius maps to CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:L — a network-reachable, low-complexity, low-privilege attack that crosses a security scope boundary. Concrete consequences include:
- Tenant data poisoning: An attacker injects spurious inventory records into a competitor or sibling business unit’s namespace, corrupting audits, license counts, and compliance reports.
- Privilege escalation via association: Accessories carry relationships to categories, locations, suppliers, and check-out assignments. Injecting records under another company can expose relationships not visible to the attacker’s own tenant context, depending on subsequent API calls.
- Audit and compliance violations: Organisations using Snipe-IT for SOX, ISO 27001, or FedRAMP asset inventory must treat any cross-tenant write as an integrity violation requiring investigation and remediation.
- Denial of data integrity: In automated procurement workflows that consume the API, injected records can trigger false approvals or block legitimate purchasing flows for the victim company.
The CVSS score of 8.5 (High) reflects the Scope:Changed metric — a single user account breaches the isolation boundary of an entirely separate organisational tenant.
How to Fix It
Upgrade immediately. The authoritative fix is to update Snipe-IT to version 8.6.2 or later:
# If self-hosted via Git
git fetch origin
git checkout v8.6.2
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
# If using Docker (official image)
docker pull snipe/snipe-it:v8.6.2
The patch applies two complementary controls:
1. Remove company_id from $fillable and force it from the authenticated session:
// FIXED — Accessory model
protected $fillable = [
'category_id',
// 'company_id' removed — set programmatically
'location_id',
'manufacturer_id',
'min_amt',
'name',
'order_number',
'purchase_cost',
'purchase_date',
'qty',
'supplier_id',
'image',
'notes',
];
2. Explicitly bind company_id from the authenticated user’s session in the controller:
// FIXED — AccessoriesController@store
public function store(Request $request)
{
$this->authorize('create', Accessory::class);
$accessory = new Accessory();
$accessory->fill($request->except(['company_id']));
// Bind company from authenticated user — never trust caller input
$accessory->company_id = Auth::user()->company_id;
$accessory->user_id = Auth::id();
if ($accessory->save()) {
return response()->json(
Helper::formatStandardApiResponse('success', $accessory, trans('admin/accessories/message.create.success'))
);
}
return response()->json(
Helper::formatStandardApiResponse('error', null, $accessory->getErrors())
);
}
For operators who cannot upgrade immediately, restrict API access to trusted network segments via firewall or reverse-proxy ACL, and audit the accessories table for records whose company_id does not match the company_id of the user in users.id = accessories.user_id.
Our Take
Mass assignment vulnerabilities are among the most persistently rediscovered flaws in web applications, and they disproportionately surface in multi-tenancy contexts precisely because the isolation requirement is a business-logic concern that no ORM can enforce automatically. Developers understand that $fillable is a security boundary for object hydration, but the instinct to include every foreign key for convenience frequently overrides that understanding when the scope of a feature is “just internal.”
The lesson here is architectural: any field that encodes ownership or tenancy — company_id, org_id, tenant_id, owner_id — must be treated identically to a privilege level. It should never appear in $fillable, and it should always be derived from the authenticated session server-side, full stop. Code review checklists and PR templates should explicitly call out mass assignment of tenancy fields as a hard block.
For enterprises running Snipe-IT or similar asset management platforms, this vulnerability is a reminder that IT management tooling is high-value target infrastructure. These platforms hold canonical records of hardware, software licenses, and organisational structure — data that is directly useful to an insider threat or a lightly privileged external attacker who has phished one API token.
Detection with SAST
This vulnerability class falls under CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) and CWE-639 (Authorization Bypass Through User-Controlled Key). In PHP/Laravel codebases, Offensive360’s SAST engine flags the following patterns:
- Taint propagation from
$request->all()or$request->input()into->fill(),::create(), or::update()without an intervening->except([...])or->only([...])call that excludes known ownership/tenancy fields. $fillablearrays on Eloquent models that include fields matching the pattern/_(id|company|tenant|org|owner)$/i— these are flagged for manual review with a rule taggedmass-assignment/tenancy-field.- Controller methods that pass request data to model persistence calls without a subsequent ownership assertion comparing the model’s tenancy field against
Auth::user()->{tenancy_field}.
The rule category in Offensive360’s taxonomy is A04:2021 – Insecure Design / Mass Assignment aligned to OWASP API Security Top 10 API6:2023 – Unrestricted Access to Sensitive Business Flows. SAST alone cannot fully close this class — DAST is required to confirm exploitability by replaying authenticated requests with manipulated tenancy identifiers across company boundaries at runtime.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-54329-class vulnerabilities and thousands of other patterns — across 60+ languages.