MDL Shield

Log-in-as via Incognito window

tool_muloginas

Print Report
Plugin Information

An alternative "Log in as" plugin that uses incognito/private browser windows to create isolated sessions for logging in as another user. It generates short-lived, single-use tokens via AJAX, which are opened in a new incognito window to establish a separate Moodle session as the target user. The plugin includes proper session lifecycle management, cleanup cron tasks, and event handlers for user logout and deletion.

Version:2026032950
Release:v5.0.6.06
Reviewed for:5.1
Privacy API
Unit Tests
Behat Tests
Reviewed:2026-04-15
23 files·1,742 lines
Grade Justification

The plugin demonstrates strong security practices throughout. The authentication mechanism uses short-lived (15-second), single-use, cryptographically random tokens with user-agent binding. Capability checks (tool/muloginas:loginas) are enforced at both the web service and helper layers. The web services are restricted to AJAX-only calls and cannot be invoked via external WS tokens. Admin users are explicitly protected from being impersonated. Session lifecycle management is thorough with proper cleanup on logout, user deletion, and via scheduled cron tasks.

The only findings are low-severity code quality and compliance issues:

  • The Privacy API implementation is contradictory — implementing both null_provider (no personal data) and metadata\provider (describes stored personal data) simultaneously
  • Direct $_SERVER['HTTP_USER_AGENT'] superglobal access where core_useragent::get_user_agent_string() should be used

No security vulnerabilities, no deprecated API usage, no SQL injection risks, no XSS vectors. The plugin includes comprehensive PHPUnit and Behat tests. Code quality is high overall.

AI Summary

Plugin Overview

tool_muloginas provides an alternative "Log in as" mechanism for Moodle administrators and managers. Instead of using Moodle's built-in log-in-as feature (which shares the browser session), this plugin generates a short-lived token that must be opened in an incognito/private browser window, creating a completely isolated session.

Architecture

The flow is:

  1. A user with tool/muloginas:loginas capability visits a target user's profile
  2. Clicking the button triggers an AJAX call (token_create) that generates a 40-character random token with a 15-second lifetime
  3. A modal displays a link that must be right-clicked and opened in a new incognito window
  4. The loginas.php page validates the token (checking lifetime, single-use, user-agent match, and that the target isn't an admin) and establishes the session
  5. A polling AJAX call (token_check) auto-closes the modal when the token is consumed

Security Assessment

The security model is well-designed:

  • Token security: 40-char cryptographically random tokens, 15-second TTL, single-use, user-agent bound
  • Access control: tool/muloginas:loginas capability required (manager archetype), admin impersonation explicitly blocked
  • Web service restrictions: AJAX-only, cannot be called from external WS clients
  • Session isolation: Uses core \core\session\manager::loginas() properly
  • Lifecycle management: Sessions destroyed on logout, user deletion handled, stale data cleaned by cron
  • Incognito detection: Pre-config.php cookie/header checks to verify fresh browser window

Code Quality

The codebase is clean, well-structured, and follows Moodle conventions. All DB queries use parameterized $DB methods. Templates use Mustache auto-escaping. The plugin includes PHPUnit tests for all core classes and a Behat test. The only issues are minor: a Privacy API interface contradiction and direct superglobal access where Moodle APIs exist.

Findings

complianceLow
Privacy API implements conflicting interfaces

The privacy provider implements both \core_privacy\local\metadata\null_provider and \core_privacy\local\metadata\provider simultaneously. These are contradictory declarations:

  • null_provider declares: "this plugin stores no personal user data"
  • metadata\provider via get_metadata() declares: "this plugin stores userid and targetuserid in tool_muloginas_request"

The plugin does store personal data — user IDs, session IDs, and user agent strings — in the tool_muloginas_request table for up to 3 days. While the data is transient, implementing null_provider prevents the privacy subsystem from processing data export or deletion requests for this plugin's data.

Risk Assessment

Low risk. The practical impact is minimal because:

  • Data retention is very short (maximum 3 days via cron, most records expire much sooner)
  • The user_deleted event handler properly cleans up records for deleted users
  • The stored data is operational metadata (session tokens), not user-generated content

However, during the retention window, the data would be invisible to Moodle's privacy subsystem for export/erasure requests, which is technically a GDPR compliance gap.

Context

The plugin stores log-in-as request records containing userid, targetuserid, sid (session ID), useragent, targetsid, and timestamps. Records are cleaned up by a cron task within 3 days, and the user_deleted event handler also removes relevant records. However, during the retention window, a GDPR data subject access request processed through Moodle's privacy API would not include or delete this plugin's data because null_provider signals the privacy system to skip this plugin entirely.

Identified Code
class provider implements
    \core_privacy\local\metadata\null_provider,
    \core_privacy\local\metadata\provider {
Suggested Fix

Remove the null_provider interface. If the data is truly transient and does not require export/deletion support, implement only metadata\provider and document the rationale. If GDPR compliance is desired, additionally implement \core_privacy\local\request\plugin\provider with export and delete methods.

class provider implements
    \core_privacy\local\metadata\provider {
code qualityLow
Direct $_SERVER superglobal access instead of Moodle API

The plugin accesses $_SERVER['HTTP_USER_AGENT'] directly in two methods where Moodle's core_useragent::get_user_agent_string() should be used instead. Direct superglobal access bypasses Moodle's abstraction layer and is less robust (e.g., no graceful handling if the header is absent).

Note: The $_COOKIE, $_SERVER['HTTP_COOKIE'], and $_SERVER['HTTP_REFERER'] accesses in loginas.php lines 41-46 occur before config.php is included, where Moodle APIs are unavailable. These are intentional and necessary for the incognito window detection logic.

Risk Assessment

Low risk. This is a code quality issue, not a security vulnerability. The direct superglobal access works correctly in practice. Using the Moodle API would improve robustness (e.g., consistent null handling) and follow Moodle coding standards.

Context

The user agent string is stored when a token is created (create_request()) and compared when the token is validated (validate_request()). This is a security measure to ensure the same browser is used for both the token creation and consumption. The comparison happens in a web request context where HTTP_USER_AGENT is virtually always present.

Identified Code
$record->useragent = $_SERVER['HTTP_USER_AGENT'] ?? '';
Suggested Fix
$record->useragent = \core_useragent::get_user_agent_string();
Identified Code
if ($request->useragent !== $_SERVER['HTTP_USER_AGENT']) {
Suggested Fix
if ($request->useragent !== \core_useragent::get_user_agent_string()) {
Additional AI Notes

The plugin includes a comprehensive test suite: PHPUnit tests for all core classes (loginas, token_create, token_check, cron) and a Behat test for the user profile integration. The tests cover both positive and negative cases including capability checks, token lifecycle, and cleanup logic.

The security architecture is notably well-designed for this type of plugin. The 15-second token lifetime, single-use enforcement, user-agent binding, admin impersonation protection, and AJAX-only web service restriction create multiple layers of defense. The incognito window detection (pre-config.php cookie checks) is a creative approach to session isolation.

The loginas.php page intentionally does not call require_login() because it establishes a new session in a fresh incognito window where no prior authentication exists. Security is instead enforced through the token mechanism, which is created by an authenticated user with the appropriate capability.

The event handler for user_loggedout (priority -1000) calls die() after rendering a custom logout page for incognito sessions. This is documented in the event observer configuration and is necessary to prevent the normal logout redirect from interfering with the incognito session closure flow.

This review was generated by an AI system and may contain inaccuracies. Findings should be verified by a human reviewer before acting on them.