Module Access Reads Were Open - edmozley/freeitsm GitHub Wiki
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
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.
-
Some secret settings were readable by any signed-in analyst. The settings request decrypted every
*_password,*_secret,*_tokenand*_api_keysetting 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.
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'sget_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.
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).
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.
| 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 |
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.
- 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_tokenandintegration_cron_tokenfromsystem_settingsand 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.
- Module Access โ Developer Guide โ the rule, corrected
- Module Access Control
- Bugs resolved