Additional tools library for MuTMS plugins
tool_mulib
A shared utility library plugin for the MuTMS suite of Moodle plugins. Provides common infrastructure including: external database connectivity (PDO-based connections to external DBs), context map caching for fast permission lookups, notification management framework, SQL fragment builder, autocomplete form element helpers, AJAX modal form helpers, upsert database operations, JSON schema validation, and various UI output components. Designed to support Moodle 5.0–5.0.2.
The plugin is generally well-written with solid security practices throughout. All page scripts enforce require_login() and appropriate capability checks (moodle/site:config for server/query management, per-component can_manage()/can_view() for notifications). Forms use Moodle’s moodleform which handles sesskey/CSRF automatically. External database queries use PDO prepared statements with named parameters. Output is sanitized using s(), format_string(), clean_text(), and format_text() as appropriate.
The most significant finding is that external database server passwords are stored in plaintext in the tool_mulib_extdb_server table. While only admins (moodle/site:config) can access this data, and this follows the same pattern as Moodle’s own enrol_database plugin, it still presents a medium-level risk if the database is compromised.
Other findings are low/info-level: direct inserts into the core role_assignments table (a design workaround), duplicate code across two utility classes, and a property name bug in query check forms. The plugin includes proper Privacy API implementation, comprehensive tests, and correct thirdpartylibs.xml declarations.
Plugin Overview
tool_mulib is a foundational shared library plugin for the MuTMS suite (MuTMS = Multi-Tenant Management Suite) of Moodle plugins. It provides:
- External database connectivity — PDO-based connections to external databases with admin-managed server configurations and parameterized queries
- Context map caching — pre-computed context hierarchy cache (
tool_mulib_context_map) for fast permission lookups, maintained via event observers and a scheduled task - Notification framework — abstract notification management with custom messages, supervisor CC, import/export, and per-component access control
- SQL fragment builder — immutable
sqlclass for safe composition of complex parameterized queries - AJAX modal forms —
ajax_form_traitand associated JS/templates for rendering moodleforms in modal dialogs - Autocomplete helpers — base classes for form autocomplete elements (users, cohorts, category contexts)
- Upsert operations — DB-engine-specific
INSERT ... ON CONFLICT/DUPLICATE KEYimplementations - JSON schema validation — wrapper around the bundled
opis/json-schemalibrary
Security Assessment
The plugin demonstrates strong security awareness. Access controls are consistently applied, prepared statements are used for external DB queries, and output escaping is thorough. The external database feature is appropriately restricted to site administrators. The notification framework delegates access control to consuming plugins via abstract can_manage()/can_view() methods.
The main concern is plaintext storage of external database credentials, which is a medium-risk issue mitigated by admin-only access. All other findings are low-severity code quality or best practice items.
Findings
The tool_mulib_extdb_server table stores external database server passwords in the dbpass column as plaintext. While access to create and view these records requires moodle/site:config (administrator-only), the unencrypted storage means that anyone with direct database access (e.g., a DB admin, a database backup recipient, or an attacker who gains read access to the DB) can retrieve these external database credentials.
Moodle does not provide a built-in encryption API for plugin settings, and other core plugins (e.g., enrol_database) follow the same pattern. However, storing credentials in plaintext increases the blast radius of a database compromise — an attacker gains access not just to the Moodle database but also to all configured external databases.
Medium risk. The finding is mitigated by the fact that only Moodle administrators can create or view server configurations through the UI. However, if the Moodle database itself is compromised (e.g., via SQL injection in another plugin, a database backup leak, or a hosting provider breach), the external database credentials are immediately available without any additional effort. This could expand a Moodle-only breach into a multi-system compromise. The risk follows the common Moodle pattern (enrol_database stores credentials similarly), but it remains a meaningful concern for organizations with compliance requirements.
The external database server configuration is managed through admin-only pages (extdb/server_create.php, extdb/server_update.php) that require moodle/site:config. The pdb class reads these credentials from the database record and passes them directly to new PDO(). The password is stored exactly as entered by the administrator.
An attacker with read access to the Moodle database can retrieve all external DB credentials:
SELECT name, dsn, dbuser, dbpass FROM mdl_tool_mulib_extdb_server;
This yields plaintext credentials for all configured external database servers.
<FIELD NAME="dbpass" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="DB user password"/>
Consider encrypting the dbpass value using \core\encryption::encrypt() before storage and \core\encryption::decrypt() on retrieval. Moodle 4.0+ provides \core\encryption for symmetric encryption using the site’s secret key. This would protect credentials at rest while remaining transparent to the admin UI.
public function __construct(stdClass $server) {
$this->dsn = $server->dsn;
$this->dbuser = $server->dbuser;
$this->dbpass = $server->dbpass;
The context_map class directly inserts records into the core role_assignments table using a magic user ID (-999666) to simulate default user role assignments. This is a workaround to make the context map’s capability lookup SQL work with Moodle’s default user role ($CFG->defaultuserroleid) and default frontpage role ($CFG->defaultfrontpageroleid).
Direct writes to core tables that the plugin does not own are generally discouraged because they can interfere with core assumptions about data integrity and may break with future Moodle updates.
Low risk. This is a design workaround, not a security vulnerability. The records are tagged with component = 'tool_mulib' for identification. The main risk is that future Moodle core changes to the role_assignments table could break this approach, or that the cleanup on uninstall might be missed. No db/uninstall.php exists to remove these records on plugin removal.
The add_default_role_hacks() method is called during context-map capability lookups to ensure the SQL joins work correctly with Moodle’s default role settings. The method checks for existing records and removes stale ones, using the component field set to tool_mulib to identify its own records. The fake user ID -999666 should never conflict with real users.
$DB->insert_record('role_assignments', [
'contextid' => $contextid,
'userid' => self::MAGIC_DEFAULT_USER_ID,
'roleid' => $roleid,
'timemodified' => time(),
'component' => 'tool_mulib',
]);
This is a pragmatic workaround. If maintained, ensure the magic user ID (-999666) and component (tool_mulib) tag are always used together so records can be identified and cleaned up. Consider documenting this in db/uninstall.php for cleanup on plugin removal.
The plugin creates records in the core role_assignments table (via the magic user ID workaround in context_map.php) and populates its own cache tables (tool_mulib_context_parent, tool_mulib_context_map). However, there is no db/uninstall.php to clean up these records when the plugin is uninstalled.
While Moodle will drop the plugin’s own tables on uninstall, the orphaned records in role_assignments (with userid = -999666 and component = 'tool_mulib') will persist.
Low risk. Orphaned records in role_assignments with a non-existent user ID are unlikely to cause functional issues, but they represent leftover data that could confuse database audits or plugin management.
The context_map::add_default_role_hacks() inserts records into role_assignments that are identified by component = 'tool_mulib' and userid = -999666. Without an uninstall script, these orphan records would remain in the database after the plugin is removed.
The classes mulib and mudb contain identical implementations of the upsert_record() method and all its helper methods (upsert_record_pgsql, upsert_record_mysql, validate_upsert_record_arguments). The mudb class appears to be the intended home for database operations, while mulib is a general utility class. Having the same complex logic duplicated creates maintenance burden and increases the risk of the two copies diverging.
Low risk. This is a code quality issue, not a security vulnerability. The duplication means bug fixes must be applied in two places, increasing the chance of inconsistency.
Both classes are in the tool_mulib\local namespace and are final classes. The context_map_builder uses mudb::upsert_record(). The mulib class version appears to be the original, with mudb added later as a dedicated database helper.
public static function upsert_record(string $table, stdClass|array $dataobject, array $uniqueindexcolumns, array $insertonlyfields = []): void {
Delegate from mulib::upsert_record() to mudb::upsert_record(), or mark one as @deprecated and update callers to use the canonical version.
public static function upsert_record(string $table, stdClass|array $dataobject, array $uniqueindexcolumns, array $insertonlyfields = []): void {
In both query_create.php and query_update.php form definitions, the query check button attempts to execute $pdb->query($data->sql, ...) but the form field is named sqlquery, not sql. The property $data->sql will be null or undefined, meaning the query check feature silently fails to test the actual SQL entered by the admin.
This is a bug that makes the "check query" button non-functional, though it does not pose a security risk.
Low risk. This is a functional bug, not a security vulnerability. The query check button likely throws an error or executes an empty query, making the admin-facing "test connection" feature non-functional for query validation.
The form field is defined as $mform->addElement('textarea', 'sqlquery', ...) on line 74 of both form classes. But definition_after_data() references $data->sql instead of $data->sqlquery. The submitted form data object will have sqlquery as the property name, not sql.
$pdb->query($data->sql, $classname::get_check_parameters());
$pdb->query($data->sqlquery, $classname::get_check_parameters());
$pdb->query($data->sql, $classname::get_check_parameters());
$pdb->query($data->sqlquery, $classname::get_check_parameters());
The pdb class uses PHP’s PDO extension directly to connect to external databases. While direct PDO usage bypasses Moodle’s $DB API, this is by design — the $DB API only supports the Moodle database, and connecting to external databases requires a separate mechanism.
This follows the same architectural pattern as Moodle core’s enrol_database plugin (which uses ADOdb for external connections). The implementation is properly secured:
- All server/query management requires
moodle/site:config - Prepared statements with named parameters are used (
$pdb->prepare()+$pdb->execute()) - Error mode is forced to
PDO::ERRMODE_EXCEPTION - Connection parameters are admin-configured, not user-supplied
Low risk. This is the correct and intended approach for external database connectivity in Moodle. The use of PDO is justified by the requirement to connect to databases that Moodle’s $DB API does not manage. All access paths are restricted to administrators.
The pdb class is instantiated from the query abstract class constructor, which loads server configuration from the tool_mulib_extdb_server table. The SQL queries executed are stored in tool_mulib_extdb_query.sqlquery and configured by administrators. Parameters are provided by the query subclass constructors (defined in code, not user input).
$this->pdo = new PDO($this->dsn, $this->dbuser, $this->dbpass, $this->dboptions);
| Library | Version | License | Declared |
|---|---|---|---|
opis/json-schema JSON Schema validation for plugin data structures (used in json_schema utility class) | 2.6.0 | Apache-2.0 | ✓ |
opis/string Unicode string manipulation dependency of opis/json-schema | 2.1.0 | Apache-2.0 | ✓ |
opis/uri URI parsing dependency of opis/json-schema | 1.1.0 | Apache-2.0 | ✓ |
Composer PHP autoloading for the bundled third-party libraries | 2.8.8 | MIT | ✓ |
The plugin has comprehensive test coverage including PHPUnit tests for most classes (context map, SQL builder, date utilities, external DB, notifications, output components, privacy, reportbuilder entities) and Behat tests for the external database server/query management workflows and the test data generator.
The Privacy API implementation is complete, implementing metadata\provider, core_userlist_provider, and plugin\provider interfaces. It correctly exports and deletes notification user records. The implementation uses get_recordset_sql() for data export, which is appropriate for potentially large datasets.
The SQL fragment builder (sql class) is a well-designed utility that prevents SQL injection by enforcing parameterized queries, rejecting dollar-sign placeholders, and providing safe query composition through immutable operations. It normalizes both named and positional parameters into named parameters internally.
The plugin demonstrates thoughtful multi-tenancy awareness throughout — the context map, capability checks, and autocomplete elements all incorporate tenant boundaries when the multi-tenancy plugin (tool_mutenancy) is active.
The AJAX modal form system properly integrates with Moodle’s form change checker, pending promises, and fragment API. The sesskey/CSRF protection is handled automatically by extending moodleform.