Skip to content

Module Access Reads Were Open

Ed Mozley edited this page Oct 2, 2026 · 1 revision

Restricted analysts could read other modules' data

Security Β· Present in 1.0.0 – 2.10.1 Β· Fixed in 2.10.2 (#2082–#2084) Β· Found while building Report Packs

πŸ› οΈ The rule this changed, for anyone adding an endpoint: Module Access β€” Developer Guide


What could happen

Module access lets an administrator decide which modules each team can use β€” a service desk team with Tickets but not Contracts, say. The restriction hid the screens, refused direct visits to a module's pages, and refused every change.

But most of the requests that read a module's data only checked that somebody was signed in. A signed-in analyst who knew β€” or guessed β€” the address of one of those requests could read the data behind a module they were never given:

  • ticket conversations, notes, attachments and audit trails
  • contracts, suppliers and supplier contacts
  • asset lists, servers and who holds each device
  • software inventories, licences and the software API keys β€” the keys the inventory agents and the Watchtower browser extension sign in with
  • change and problem records, workflows, process maps and network diagrams
  • the system log

They stayed inside the companies they could already access, so this was never a leak between companies. It needed somebody already signed in to FreeITSM, and only mattered on installs that restrict modules.

Two smaller problems found on the way

  • Some secret settings were readable by any signed-in analyst. The settings request decrypted every *_password, *_secret, *_token and *_api_key setting but masked only a fixed list, so the satisfaction-survey signing key (csat_token_secret) and the five cron tokens went out in plain text. With the survey key someone could submit ratings on a customer's behalf; with a cron token, start that scheduled job over the web. Passwords and AI keys were always masked.
  • Four Tickets screens had no access check of their own β€” CSAT, Dashboard, the Widget Library and the Triage queue. The shared header checks sign-in, but only after the page has started sending, so its redirect could not work and the screens rendered β€” empty β€” to somebody not signed in.

Why it was like that

When module access was enforced (#30, July 2026, before 1.0.0), the design deliberately guarded pages and writes and left reads open:

"Guard the module's write endpoints. Do not blanket-guard reads that other modules/flows depend on β€” e.g. get_analysts, get_tenants, branding, the login page's get_sso_providers."

That reasoning was sound for shared reads β€” the analyst picker is used in six modules, and guarding it with one of them breaks the other five. But it was applied to all reads, including the great majority used by only one module. The developer guide taught the same rule, so every endpoint written since followed it.


How it was fixed

All 188 unguarded analyst reads were classified by their real callers β€” not by their folder: grep the file name, check which API_BASE each hit uses, and for a shared assets/js/ file find which pages load it. Four agents worked through them in parallel and every verdict was checked against the code before it was applied.

Verdict Endpoints Guard
Used by one module 144 requireModuleAccessJson('<module>')
Shared by several 12 requireAnyModuleAccessJson([...]) naming every module that calls it
Administrators only 2 (+4 System reads) requireModuleAccessJson('system') β€” analystIsAdmin()
Must stay open 19 Listed with the reason: token feeds, webhooks, your own account, the documents panel and global search (both filter each result themselves)
Nothing calls it 6 Deleted β€” including a debug page that printed the request and an attachment's file path

Plus:

  • Settings: a secret by name that is not on the mask list is no longer sent at all, and the request needs Assets, Software, Tickets or System (its four callers).
  • Companies: the full list (every company and its email domains) needs System or Tickets; everybody else already asks for the companies I can access.
  • Saved table views check the module the table belongs to.
  • The four Tickets pages got requireModuleAccess('tickets').

Writes were checked too: every write was already guarded or exempt for a stated reason (sign-in, own account, documents check their parent record, the generic settings writer checks each key).

So it cannot drift back

tests/module-access-coverage.php walks every file under api/ and fails on any endpoint with neither a guard nor an entry in its allow-list β€” and each entry carries its reason. It also fails if an allow-listed file has since gained a guard, or no longer exists. Run against 2.10.1 it fails on 167 endpoints and the four pages.


πŸ“ Files

File Change
157 files under api/ One guard line each, after the sign-in check
api/settings/get_system_settings.php, includes/encryption.php Unmasked secrets not sent; isSecretSettingName()
api/system/get_tenants.php The full list needs System or Tickets
api/table-views/list.php, save.php Guarded by the table's module
tickets/csat/, tickets/dashboard/ (+ library.php), tickets/triage/ Page guard
6 dead endpoints Deleted
tests/module-access-coverage.php New

How it was verified

Every changed request was driven over HTTP as an analyst who has Assets, LMS and Tickets only:

  • 158 / 158 answered as their guard says β€” allowed where it names Tickets or Assets, refused everywhere else, no PHP errors.
  • Signed out, every one refused.
  • The settings request no longer returned the six secrets, and still returned its other 188 settings.
  • The control: the same run against 2.10.1 let that analyst into 91 endpoints for modules he does not have, sent him the survey key and cron tokens, and served the Triage page to a signed-out request.

What this means for you

  • Upgrade. Every release from 1.0.0 to 2.10.1 is affected.
  • If an analyst you do not trust could have signed in before you upgraded, replace the secrets: delete the rows csat_token_secret, sla_cron_token, webhook_cron_token, workflow_cron_token, domain_cron_token and integration_cron_token from system_settings and run System β†’ Database Verification, which creates new ones. Survey links already emailed stop working, and a scheduled job started by web address needs its new address (php scripts/cron_token.php --url).
  • If an analyst now sees "You do not have access to this feature" somewhere they used to work, their team is missing that module β€” give it in System β†’ Teams β€” or tell me, if they should not need it for that screen.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally