Skip to content

Access Outlived Its Removal

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

Security: 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


The short version

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.

What was wrong

All three are one mistake: a decision was copied somewhere at the moment access was granted, and the copy was trusted afterwards.

1. The administrator flag, cached at sign-in

requireAdminJson($conn);   // asks the database, every request
sessionIsAdmin();          // read $_SESSION['is_admin'], cached at sign-in

The 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

2. The module list, cached at sign-in

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.

3. Tokens that never asked about their owner

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.

The fix

Ask the database, at the moment of use. Never a copy.

  • sessionIsAdmin() now calls analystIsAdmin(), which caches its answer for one request β€” so a page that asks twenty times still asks once.
  • analystAllowedModulesNow() (new, in includes/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 under api/ or includes/ reads $_SESSION['allowed_modules'] again.
  • A deactivated or deleted analyst is signed out on their next request β€” includes/analyst_active_gate.php, loaded by functions.php beside 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.

πŸ“ Files changed

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

Tested

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 = 1 is not an administrator; an administrator whose session says 0 is 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/ or includes/ 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.

Do I need to do anything?

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.

For developers

  • 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: use sessionIsAdmin(), requireAdminJson(), analystAllowedModulesNow() or analystCanAccessModule().
  • 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_active check of its own; the gate has done it before your code runs.

Related: A link could change data Β· Analysts leaving Β· Bugs resolved

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally