Skip to content

License product keys exposed to users lacking the viewKeys permission via bulk CSV export and via an API existence oracle

Moderate
snipe published GHSA-5jcj-c9p3-82q7 Aug 24, 2026

Package

No package listed

Affected versions

<= 8.6.3

Patched versions

8.7.0

Description

Snipe-IT models "read the raw license serial (product key)" as a distinct authorization concern from "list and view license records." The gate is LicensePolicy::viewKeys(), which grants access only to users holding licenses.keys, licenses.create, or licenses.edit, and the API transformer at app/Http/Transformers/LicensesTransformer.php honors it correctly:

'product_key' => (Gate::allows('viewKeys', $license)) ? e($license->serial) : '------------',

Two other code paths that touch licenses.serial were missed and only enforce the coarser view gate (licenses.view).

Vector A: bulk cleartext extraction via CSV export

LicensesController::getExportLicensesCsv() (route licenses.export, GET /licenses/export) authorizes only view on the License model and then streams the serial column into the CSV without further checks:

public function getExportLicensesCsv()
{
    $this->authorize('view', License::class);
    // ...
    foreach ($licenses as $license) {
        $values = [
            $license->id,
            $license->company ? $license->company->name : '',
            $license->name,
            $license->serial,
            // ...
        ];
        fputcsv($handle, $formatter->escapeRecord($values));
    }
}

A user holding only licenses.view can request this endpoint and receive every product key in the instance in cleartext, one row per license, subject only to the standard FMCS company scope. Values are emitted plain (they are not passed through the product_key masking that the transformer applies), so the attacker walks away with the same key material the license owner would need to activate the software.

Vector B: existence oracle on the API index

Api\LicensesController::index() (route api.licenses.index, GET /api/v1/licenses) authorizes view and then honors two filter shapes that both read from licenses.serial:

if ($request->filled('product_key')) {
    $licenses->where('licenses.serial', '=', $request->input('product_key'));
}
// ...
if ($request->filled('filter') || $request->filled('search')) {
    $licenses->TextSearch($request->input('filter') ? $request->input('filter') : $request->input('search'));
}

License::$searchableAttributes includes serial, so TextSearch performs a LIKE %term% against the raw column as well. The response body correctly masks product_key back to ------------ for callers who fail viewKeys, but the total count and the presence or absence of rows in the payload leak whether the queried value matched. A caller can iterate candidate keys (or substrings) and confirm which ones are valid by comparing total=0 against total>0, converting any partial leak from logs, backups, screenshots, or vendor documentation into a validated key.

Both vectors share the same root cause: the serial column is authorized-adjacent, so every read, filter, search, and export path against it must consult viewKeys in addition to the base view gate. The masking in the transformer is necessary but not sufficient.

Impact

Any authenticated user holding only the routine licenses.view permission can

  • download every license key in the instance in bulk as CSV (Vector A), and
  • validate candidate keys one at a time against the API without needing to see the masked value in the response (Vector B).

License product keys are secrets under normal Snipe-IT deployment assumptions. Their disclosure carries direct financial impact (relicensing costs, vendor audits) and, for BYOL-style deployments where the key doubles as an activation credential, enables unauthorized use of the licensed software.

Patches

Fixed in commit c4d3a95. The CSV export now emits the shared License::PRODUCT_KEY_MASK placeholder (------------, same mask the API transformer already used) in the serial column for callers who fail the viewKeys gate. The API index silently drops the product_key filter for those callers (so a valid vs invalid candidate return the same result set, killing the oracle), and its search / filter paths run through a new License::scopeTextSearchWithoutSerial scope that removes serial from the searchable attribute set before delegating to the standard TextSearch. The same mask constant is also used by AssetsTransformer::transformLicenseCheckedToAsset for consistency across every surface that renders a masked product key.

Regression coverage in tests/Feature/Licenses/Ui/ExportLicensesCsvTest.php and tests/Feature/Licenses/Api/LicenseIndexTest.php pins: CSV export mask presence for view-only callers, real values for licenses.keys / licenses.edit / superuser callers, product_key filter suppression, structured filter suppression on the serial key, free-text search suppression on serial values, and restoration of the searchable attribute set after the scope returns.

Workarounds

Until the patch lands, deployments should ensure only trusted users hold the licenses.view permission, or additionally restrict access to the /licenses/export route and the product_key, search, and filter query parameters on /api/v1/licenses at the web-server or WAF layer.

Credits

Reported by Pavel Kohout (Aisle Research), GitHub handle pavelkohout396.

Metadata

  • CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (6.5 Medium)
  • CWE: CWE-863 (Incorrect Authorization), with CWE-204 (Observable Response Discrepancy) applying to Vector B specifically
  • Affected: <= 8.6.3 (both vulnerable code paths predate v3.x and have existed for the entire supported life of the project)
  • Patched: 8.7.0
  • Fix commit: c4d3a95

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Observable Response Discrepancy

The product provides different responses to incoming requests in a way that reveals internal state information to an unauthorized actor outside of the intended control sphere. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits