Missing Authorization + Mass Assignment on Plugin Install Endpoint -- Any Authenticated User Can Deploy Marketplace Plugins
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:
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:
foreach ($pluginProps as $key => $value) {
$builder->set($key, $value); // no allowlist
}Reproduction steps
Proof of concept:
- Authenticate as any non-admin user (editor, commenter, read-only).
- Obtain the session CSRF token from any page.
- POST directly to the install endpoint:
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():
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
Source: Leantime/leantime