Privileged sessions (aka sudo)
tool_musudo
Privileged sessions (aka sudo) plugin for Moodle LMS. Allows site administrators to configure specific non-admin users who can temporarily elevate their permissions by switching to designated roles in designated contexts, similar to Unix sudo. Supports optional MFA verification before privilege activation. Session regeneration, event logging, and an after_config hook enforce security invariants. Management is restricted to site admins, and the sudoer list is presented via report builder.
The plugin is well-structured, security-conscious, and follows Moodle coding standards throughout.
All pages enforce proper access controls:
- Management pages (
index.php,sudoer_create.php,sudoer_update.php,sudoer_delete.php) useadmin_externalpage_setup()withmoodle/site:configcapability, and the create/update/delete endpoints additionally requireis_siteadmin()viautil::require_admin(). - Sudo start uses
require_login()plussudoer::can_sudo()which validates the user is a configured sudoer, not an admin, not logged-in-as, and not in CLI/WS context. - Sudo end uses
require_login(),can_sudo(), andrequire_sesskey()for CSRF protection. - The web service (
sudoer_create_userid) validates context and requires site admin.
All database access uses Moodle's $DB API with parameterized queries. Output escaping is handled correctly: s() for the note field in report builder, format_string() for role names (via core role_get_name()), and Mustache double-brace auto-escaping in templates. The Privacy API implementation is complete and well-tested. Session regeneration on sudo start prevents session fixation. Events are logged for all security-relevant actions (sudo start, end, sudoer create, update, delete).
The only finding is a low-severity code quality bug: missing global $SESSION declarations in start_sudo() and end_sudo(), which silently breaks MFA state preservation. This does not create a security vulnerability — it actually makes the system slightly more strict by not preserving MFA authentication state across session regenerations.
The plugin includes comprehensive PHPUnit tests covering all core functionality (CRUD, access control, events, privacy provider, web service) and Behat tests covering the full user workflows including MFA scenarios.
Plugin Overview
tool_musudo implements a "sudo" mechanism for Moodle, allowing site administrators to grant specific non-admin users the ability to temporarily elevate their privileges by switching to designated roles in designated contexts. This is analogous to the Unix sudo command.
Architecture
The plugin is cleanly organized:
- Core logic in
classes/local/sudoer.phphandles CRUD operations, sudo session start/end, session management, and hook callbacks - MFA integration in
classes/local/mfa.phpdelegates totool_mfafactor verification - Forms in
classes/local/form/usetool_mulib's AJAX form base class - Report builder entity and system report for the admin sudoer list
- Privacy API with full GDPR provider implementation
- Events for all security-relevant actions
Security Design
The security model is well-designed:
- Only site admins can manage sudoers (enforced via
is_siteadmin()check, not just capability) - Site admins cannot sudo themselves (explicitly excluded in
can_sudo()) - CLI scripts and web service contexts are excluded
- "Login as" sessions are excluded
- Session regeneration occurs on sudo start to prevent session fixation
- CSRF protection via
require_sesskey()on the sudo end endpoint - MFA verification can be required per-sudoer, with bruteforce lockout protection
after_confighook detects inconsistent state (sudo active but all role switches removed) and forces sudo termination- Event logging provides audit trail
Test Coverage
Comprehensive test suite including:
- PHPUnit tests for sudoer CRUD, access control logic, event triggering, MFA helpers, utility functions, privacy provider, and web service
- Behat tests for management workflow (add/update/delete sudoers) and sudo session workflow (start/end, MFA verification, lockout scenarios)
Findings
Both start_sudo() and end_sudo() in the sudoer class reference the $SESSION global variable without declaring it with global $SESSION. The global declarations at the top of each method only include $DB and $USER.
This means all code that attempts to read from or write to $SESSION is operating on an undefined local variable, not the actual PHP session object:
isset($SESSION->tool_mfa_authenticated)always returnsfalsebecause$SESSIONis undefined in the local scope (PHP'sisset()silently handles undefined variables)$SESSION->tool_mfa_authenticated = $tmawould create a localstdClassobject rather than modifying the actual session
The code's intent is to preserve the user's MFA authentication state across session regeneration (in start_sudo()) and session restart (in end_sudo()), but this preservation silently never occurs.
Low risk. This is a functional bug, not a security vulnerability. The consequence is that users may be forced to re-authenticate with MFA after starting or ending a sudo session, if global MFA is enabled on the site. This makes the system more strict than intended (requiring extra authentication rather than bypassing it), so the security impact is neutral-to-positive. The user experience impact is that sudoers on MFA-enabled sites may face unnecessary MFA re-prompts.
When a sudo session starts or ends, the plugin regenerates or restarts the user's PHP session to get clean session caches. Before doing so, it attempts to save the tool_mfa_authenticated flag from $SESSION and restore it afterward, so the user doesn't lose their MFA authentication state.
Without the global $SESSION declaration, the preservation code is dead code. The MFA state from the global session is never read (because the local $SESSION is undefined), and it is never restored after session regeneration.
public static function start_sudo(): bool {
global $DB, $USER;
// ...
// Keep previous MFA state.
if (isset($SESSION->tool_mfa_authenticated)) {
$tma = $SESSION->tool_mfa_authenticated;
} else {
$tma = null;
}
// ... session regeneration ...
if (isset($tma)) {
$SESSION->tool_mfa_authenticated = $tma;
}
Add $SESSION to the global declaration:
public static function start_sudo(): bool {
global $DB, $USER, $SESSION;
public static function end_sudo(): void {
global $DB, $USER;
// ...
// Keep previous MFA state.
if (isset($SESSION->tool_mfa_authenticated)) {
$tma = $SESSION->tool_mfa_authenticated;
} else {
$tma = null;
}
// ... session restart ...
if (isset($tma)) {
$SESSION->tool_mfa_authenticated = $tma;
}
Add $SESSION to the global declaration:
public static function end_sudo(): void {
global $DB, $USER, $SESSION;
The plugin depends on tool_mulib (a shared library plugin from the same MuTMS suite) and tool_mfa (Moodle core's MFA plugin). The tool_mulib dependency provides the ajax_form base class, sql query builder, and form autocomplete user search helpers. Since tool_mulib is a separate plugin, its code was not reviewed here, but the integration points appear well-designed with proper parameter validation and access control.
The plugin uses Moodle's internal role switch (rsw) mechanism to implement privilege elevation. By setting $USER->access['rsw'][$context->path] = $roleid, it leverages the same infrastructure that Moodle's built-in "Switch role to..." feature uses. This is a clever approach that integrates cleanly with Moodle's access control system without needing custom capability checking logic.
The after_config hook (\core\hook\after_config) runs on every page load and detects when a sudo user's role switches have been emptied (e.g., by using Moodle's native "Return to my normal role" in a course). When this inconsistent state is detected, the hook forces a redirect to the sudo end page, preventing users from being stuck in a partially-elevated state. The TOOL_MUSUDO_END_SCRIPT constant prevents redirect loops on the end page itself.
The util::require_admin() method enforces a stricter access check than Moodle's require_admin() alone. Core's require_admin() checks moodle/site:config capability, which could be granted to non-admin roles. The plugin additionally verifies is_siteadmin() to ensure only actual site admins (listed in $CFG->siteadmins) can manage sudoers. This is appropriate for a security-sensitive plugin that grants privilege escalation.
The plugin includes comprehensive test coverage: 8 PHPUnit test classes covering CRUD operations, access control, events, MFA helpers, utility functions, privacy provider, and the web service; plus 3 Behat scenarios covering the full management workflow and sudo session lifecycle including MFA verification and lockout.