#3757·leantime

Missing Authorization + Mass Assignment on Plugin Install Endpoint -- Any Authenticated User Can Deploy Marketplace Plugins

Author: geo-chenCreated Aug 25, 2026Updated Sep 7, 2026
LabelsFixed and Staged

What is your set up?

Self Hosted Docker

Version

3.9.5

Describe the issue

reported via email on 4 July 2026:

A non-admin authenticated user can install marketplace plugins onto a Leantime instance by posting directly to the HTMX plugin install endpoint, bypassing the admin/owner role gate that protects the marketplace UI. The backing service also applies no property allowlist when constructing the plugin model from request data, allowing the attacker to supply arbitrary plugin identifiers, versions, and license keys.

Affected version: 3.9.5 (current HEAD) CVSS 3.1: 7.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L) CWE: CWE-862 (Missing Authorization), CWE-915 (Mass Assignment)

Root cause:

Controllers/Marketplace.php::get() enforces the admin/owner restriction:

php
Auth::authOrRedirect([Roles::$owner, Roles::$admin], true);

Hxcontrollers/Details.php::install() has no such check and no #[RequiresPermission] attribute. The PermissionEnforcer returns early (no-op) when no attribute is present, leaving the endpoint open to any authenticated session.

Plugins::buildMarketplacePluginFromRequest() iterates all request keys without an allowlist:

php
foreach ($pluginProps as $key => $value) {
    $builder->set($key, $value);  // no allowlist
}

Reproduction steps

Proof of concept:

  1. Authenticate as any non-admin user (editor, commenter, read-only).
  2. Obtain the session CSRF token from any page.
  3. POST directly to the install endpoint:
bash
curl -X POST https://leantime.example.com/hx/plugins/details/install \
  -b "leantime_session=<non-admin-session>" \
  -H "X-CSRF-TOKEN: <token>" \
  -d "plugin[identifier]=leantime-ai&plugin[version]=1.0.0&plugin[license]=any-key"

The server makes an outbound request to the configured marketplace URL, downloads the plugin zip, extracts PHP files into the server's plugin directory, and registers the plugin in the database -- without verifying the caller's role.

Fix:

Add role enforcement to Details::install():

php
public function install(): string
{
    Auth::authOrRedirect([Roles::$owner, Roles::$admin], true);
    // ...
}

Restrict buildMarketplacePluginFromRequest() to an explicit property allowlist excluding type, marketplaceUrl, and other security-sensitive properties.

Error Logs (LEANTIMEFOLDER/storage/logs)

No response