Repository navigation
Access Outlived Its Removal
Fixed in 3.15.1 Β· Reported privately by a security researcher, who asked not to be named Β· Present in every release up to and including 3.15.0 Β· Modules: System, Documents, Search, Calendar, Tickets, Watchtower
Taking access away from somebody did not fully take effect while they still had something open β a browser session, or a calendar subscription. Three versions of the same mistake:
- A demoted administrator kept a few administrator-only abilities until they signed out: the Slack settings, approving any catalogue request, adding or removing management lines.
- An analyst who lost a module β Contracts, say β could still download, attach and find that module's documents, and see its records in search, until they signed out. And a deactivated analyst's session kept working on every page that only asked "is somebody signed in?".
- A deactivated analyst's calendar and schedule feed links kept working, and so did their Watchtower extension key. These are not sessions at all, so signing out would not have stopped them either.
Who could do it: only somebody who had been given the access in the first place. Nobody gained anything they had not been given. The risk is the one you would expect: you remove someone's access because they are leaving, or because an account was misused, and it is not quite removed.
Upgrading is the fix. Nothing else needs doing β open sessions and existing feed links are re-checked from the next request.
All three are one mistake: a decision was copied somewhere at the moment access was granted, and the copy was trusted afterwards.
requireAdminJson($conn); // asks the database, every request
sessionIsAdmin(); // read $_SESSION['is_admin'], cached at sign-inThe second was written for pages β "should this person see the System card?" β and its comment said so: cosmetic; for hard enforcement on APIs use requireAdminJson(). But a convenient function gets used, and four places came to rely on it as a real gate:
| Where | What it let a demoted admin do |
|---|---|
api/messaging/get_channels.php, save_channel.php
|
read and edit Slack channel settings |
includes/messaging/messaging.php (messagingAdminMayAdministerChannel) |
test, diagnose and delete a Slack channel |
includes/catalogue_approvals.php |
approve or reject anybody's catalogue request |
includes/managers.php (managerLinesEditable) |
add and remove management lines |
Sign-in copied the analyst's modules into $_SESSION['allowed_modules']. The module pages re-read the grants every time, so they closed at once. But eight document endpoints β download, attach, save, unlink, find, links, list, Help Centre documents β and global search read the copy:
$allowed = $_SESSION['allowed_modules'] ?? null;The document rules decide "may you see this document?" by "may you see the module of the record it hangs off?" β so for these, the stale copy was the module gate. Removing a team's grant, changing a team's modules, or switching the permission mode never refreshed it.
And separately: deactivating an analyst clears analysts.is_active, which every module gate re-reads β but a few hundred endpoints begin with nothing more than if (!isset($_SESSION['analyst_id'])), and for those a deactivated analyst with a live session was still signed in.
The calendar feed (api/calendar/feed.php), the schedule feed (api/tickets/schedule_feed.php) and the Watchtower extension (api/watchtower/get_dashboard_ext.php) look up a token and act as its analyst. None of them asked whether that analyst was still active, or still had the module. The REST API already did β it refuses a key whose owner is inactive.
The Watchtower one had a second problem. Its keys live in the same table as the software inventory agents' keys, which are deployed onto machines. So an agent's key opened the whole Watchtower dashboard β and a key with no analyst behind it saw every company.
Ask the database, at the moment of use. Never a copy.
-
sessionIsAdmin()now callsanalystIsAdmin(), which caches its answer for one request β so a page that asks twenty times still asks once. -
analystAllowedModulesNow()(new, inincludes/functions.php) reads the analyst's modules from the database, cached for the request, failing closed. Every reader of the session copy uses it instead, including the launcher, so a removed module also disappears from the menu. A test fails if anything underapi/orincludes/reads$_SESSION['allowed_modules']again. -
A deactivated or deleted analyst is signed out on their next request β
includes/analyst_active_gate.php, loaded byfunctions.phpbeside the password-change gate, so no endpoint has to remember it. One primary-key lookup per signed-in request. It destroys the session rather than refusing one request, and leaves the sign-in and sign-out pages alone so a stale session can always leave. -
The feeds and the Watchtower key check their owner with
analystCanAccessModule(), which is already false for an inactive or deleted analyst β one call answers both "still active?" and "still allowed?". Checking at use rather than deleting the token means reactivating someone restores their links. A Watchtower key with no analyst is refused.
π The sign-out gate never signs anybody out on a database error. Setup and Database Verification run before the analysts table is complete, and "I could not ask" is not evidence that someone was deactivated. The module and administrator gates still fail closed, so nothing opens up when the lookup fails.
| File | Change | |
|---|---|---|
| π¦ | includes/functions.php |
sessionIsAdmin() asks the database; new analystAllowedModulesNow(); loads the new gate |
| π© | includes/analyst_active_gate.php |
new β signs out a deactivated or deleted analyst |
| π¨ |
api/documents/*.php (8), api/system/global_search.php, includes/search/search.php, includes/display_order.php
|
the modules now, not the copy |
| π§ |
api/calendar/feed.php, api/tickets/schedule_feed.php, api/watchtower/get_dashboard_ext.php
|
check the token's owner |
tests/security-report-2026-10.php sections 2β3, and tests/security-report-2026-10-part2.php sections 10β11, against the real database and web server:
- A non-administrator whose session still says
is_admin = 1is not an administrator; an administrator whose session says0is one (the positive control). - A session for an analyst who does not exist is refused with 401, and its session file is gone afterwards. An active analyst on the same endpoint is answered.
- A session that still says "every module" for an analyst who has been restricted gets the restricted list; nothing under
api/orincludes/reads the session copy. - A deactivated analyst's calendar feed is refused; an active analyst's is served (the positive control). A deactivated analyst's Watchtower key, and a key with no analyst, are refused.
Upgrade. If you recently removed somebody's access and are worried, the fix applies to sessions and links that already exist. If you used an inventory agent's key for the Watchtower extension, generate a key from an analyst account with Watchtower instead.
-
Never decide anything from a copy of a permission.
$_SESSION['is_admin']and$_SESSION['allowed_modules']are still written, for old code, but nothing should read them for a decision: usesessionIsAdmin(),requireAdminJson(),analystAllowedModulesNow()oranalystCanAccessModule(). -
A token that acts as an analyst checks the analyst β
analystCanAccessModule()at the moment of use. - An endpoint open to any signed-in analyst needs no
is_activecheck of its own; the gate has done it before your code runs.
Related: A link could change data Β· Analysts leaving Β· Bugs resolved
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96