MDL Shield

Interactive book

mod_mubook

Print Report
Plugin Information

An interactive book activity module for Moodle that supports multiple content types (HTML, Markdown, unsafe raw HTML, and disclosure/collapsible sections). It provides chapters with sub-chapters, tagging, backup/restore, and an extensible hook system for registering additional content types. Depends on `tool_mulib` for UI components (ajax forms, dropdowns). Bundles league/commonmark for Markdown rendering.

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

The plugin demonstrates strong security practices throughout. All pages enforce require_login() and require_capability() before any privileged operation. Database access uses Moodle's $DB API with parameterized queries exclusively. Output is properly sanitized via format_text(), format_string(), and clean_text(). Sesskey validation is handled automatically by moodleform and ajax_form. File handling uses the Moodle File API correctly. The unsafehtml content type is properly gated behind mod/mubook:usexss (managers only by default), and restore security correctly resets trust for cross-site backups.

The few findings are all low severity:

  • A bug in the tagged chapters SQL query that references the wrong module name (book instead of mubook), causing tag search to fail silently.
  • Use of triple-mustache (unescaped output) in the disclosure template for text labels that should use double-mustache.
  • Unescaped content type name in the unknown content rendering path, though the audience is restricted to editing teachers.

None of these represent exploitable security vulnerabilities in normal usage. The codebase is well-structured with comprehensive test coverage (PHPUnit and Behat), proper Privacy API implementation, full backup/restore support, and thoughtful extensibility through hooks.

AI Summary

Plugin Overview

mod_mubook is a well-engineered interactive book module that extends Moodle's content capabilities. It supports:

  • Multiple content types: HTML (via Atto/TinyMCE editor), Markdown (via bundled league/commonmark 2.8), unsafe raw HTML (for trusted admins), and disclosure/collapsible sections
  • Hierarchical chapters: Top-level chapters with one level of sub-chapter nesting
  • Extensibility: Hook-based system (content_classes, content_post_render, book_actions, chapter_actions, content_actions) allowing other plugins to register new content types and actions
  • Security model: Proper capability-based access control with mod/mubook:usexss gating unsafe content behind RISK_XSS

Security Assessment

The plugin follows Moodle security best practices consistently:

  • Authentication/Authorization: Every page calls require_login() or require_course_login() followed by appropriate require_capability() checks. Additional per-object can_create/can_update/can_delete/can_view methods provide fine-grained control.
  • Input validation: Uses required_param()/optional_param() with proper PARAM_* types. Form data processed through moodleform.
  • Output sanitization: HTML content passes through format_text() with noclean => false. Markdown output is sanitized via clean_text() after conversion. Chapter titles use format_string().
  • CSRF protection: All state-changing operations use moodleform or ajax_form which handle sesskey automatically.
  • Restore security: Unsafe content trust is properly reset for cross-site restores unless explicitly enabled by admin.

Code Quality

The codebase is clean and well-organized with proper separation of concerns. The plugin includes comprehensive PHPUnit and Behat tests, proper event logging, full backup/restore support, and a null Privacy API provider (appropriate since the module stores no personal data).

Findings

code qualityLow
Tagged chapters SQL query references wrong module name

The SQL query in mod_mubook_get_tagged_chapters() joins with the {modules} table using m.name='book' instead of m.name='mubook'. This means the tag search feature for mubook chapters will never return results, because it looks for course modules belonging to the core book module rather than mubook.

This is a functional bug inherited from copying the core book module's tag implementation without updating the module name reference.

Risk Assessment

Low risk. This is a functional bug, not a security issue. The tag search for mubook chapters will silently return no results because it filters on the wrong module name. No data is exposed or modified incorrectly — the feature simply doesn't work. The fix is a one-character change from 'book' to 'mubook'.

Context

The mod_mubook_get_tagged_chapters() function in locallib.php is a callback used by Moodle's tag system to find book chapters tagged with a specific tag. The SQL query joins several tables to find matching chapters and their associated course modules. The {modules} table join is used to ensure the course module belongs to the correct activity type.

Identified Code
JOIN {modules} m ON m.name='book'
Suggested Fix

Change the module name to mubook:

JOIN {modules} m ON m.name='mubook'
best practiceLow
Disclosure template uses unescaped output for text labels

The disclosure.mustache template renders user-provided text labels using triple-mustache syntax ({{{labelshow}}}, {{{labelhide}}}, {{{labelprinted}}}), which outputs content without HTML escaping. These labels are plain text, not HTML, and should use double-mustache ({{labelshow}}) for automatic HTML escaping.

In normal operation, the labels are cleaned by PARAM_TEXT during form submission, which strips HTML tags. However, during backup/restore, the data1 field (containing JSON with these labels) is restored verbatim from the backup XML, bypassing form validation. A maliciously crafted backup file could inject HTML/JavaScript into these labels.

Risk Assessment

Low risk. The attack vector is very constrained — it requires an administrator to restore a maliciously crafted backup file. Normal form-based input is properly sanitized via PARAM_TEXT. The fix is trivial (switch to double-mustache) and would eliminate the theoretical risk entirely. Since this is a defense-in-depth issue rather than an actively exploitable vulnerability, low severity is appropriate.

Context

The disclosure content type stores custom button labels as JSON in the data1 database field. The disclosure.php render method decodes this JSON and passes the labels directly to the Mustache template. During normal form submission, PARAM_TEXT cleaning strips HTML tags from the labels. During backup/restore, the data1 field is transferred without re-validation.

Identified Code
<span class="d-print-none label-collapsed">{{{labelshow}}}</span>
<span class="d-print-none label-expanded">{{{labelhide}}}</span>
<span class="d-print-inline hidden">{{{labelprinted}}}</span>
Suggested Fix

Use double-mustache for automatic HTML escaping since these are text labels, not pre-rendered HTML:

<span class="d-print-none label-collapsed">{{labelshow}}</span>
<span class="d-print-none label-expanded">{{labelhide}}</span>
<span class="d-print-inline hidden">{{labelprinted}}</span>
code qualityLow
Content type name not HTML-escaped in unknown content rendering

The unknown content type renders the content type name using get_string() with an {$a} placeholder, which does not HTML-escape the replacement value. The $this->record->type value is passed directly into the language string and output as HTML.

In normal operation, content types are validated as PARAM_ALPHANUM during creation, making this safe. However, during backup/restore, the type field is restored from backup XML without re-validation. Additionally, only users with mod/mubook:editcontent capability can see unknown content (restricted in unknown::can_view()).

Risk Assessment

Low risk. The combination of constraints makes this very difficult to exploit:

  1. Content types are validated as PARAM_ALPHANUM during normal creation
  2. The only injection vector is through a malicious backup restore
  3. Only users with editing capabilities can see the unknown content rendering
  4. The DB column is CHAR(100), limiting payload size

This is a defense-in-depth issue. Adding s() escaping is a simple best-practice fix.

Context

The unknown content class is a fallback used when a content record's type doesn't match any registered content class (e.g., after uninstalling an extension that added a custom content type). The can_view() method restricts visibility to users with the mod/mubook:editcontent capability, which limits the audience to teachers, editing teachers, and managers.

Identified Code
return '<div class="text-danger">' . get_string('content_unknowntype', 'mod_mubook', $this->record->type) . '</div>';
Suggested Fix

HTML-escape the type value before passing to get_string():

return '<div class="text-danger">' . get_string('content_unknowntype', 'mod_mubook', s($this->record->type)) . '</div>';
Third-Party Libraries (10)
LibraryVersionLicenseDeclared
league/commonmark
Markdown to HTML conversion engine, used for the Markdown content type rendering
2.8.0BSD-3-Clause
dflydev/dot-access-data
Dependency of league/commonmark for dot-notation data access in configuration
3.0.3MIT
league/config
Dependency of league/commonmark for configuration management
1.2.0BSD-3-Clause
nette/schema
Dependency of league/config for schema validation
1.3.3BSD-3-Clause
nette/utils
Dependency of nette/schema for utility functions
4.0.10BSD-3-Clause
psr/event-dispatcher
PSR-14 event dispatcher interface, dependency of league/commonmark
1.0.0MIT
symfony/deprecation-contracts
Symfony deprecation handling, dependency of league/commonmark
3.6.0MIT
symfony/polyfill-php80
PHP 8.0 polyfills for older PHP versions, dependency of league/commonmark
1.33.0MIT
pomodocs/commonmark-alert
CommonMark extension for GitHub-style alert blocks (note, tip, important, warning, caution)
dev-mutms-php81MIT
mutms/commonmark-extra
Custom CommonMark extension with additional Markdown features (task lists, etc.)
dev-mainMIT
Additional AI Notes

The plugin has a well-designed extensibility model using Moodle's hook system. Other plugins can register new content types via the content_classes hook, add actions via book_actions/chapter_actions/content_actions hooks, and modify rendered content via content_post_render. This is a clean architectural pattern that avoids tight coupling.

The unsafe HTML content type (unsafehtml) is properly security-gated. It requires mod/mubook:usexss capability (managers only by default, marked with RISK_XSS). During restore from other sites, the unsafetrusted flag is automatically reset to 0 unless the admin explicitly enables cross-site trust via the restoreothertrustunsafe setting. This is a thoughtful security design.

The plugin includes comprehensive test coverage with both PHPUnit tests (events, hooks, content types, generators, formatters, privacy) and Behat tests (chapters, content types, navigation). This level of testing is above average for Moodle plugins.

The Privacy API uses null_provider, which is appropriate since the plugin's database tables (mubook, mubook_chapter, mubook_content) store course content rather than personal user data. User activity tracking is handled by Moodle core (events, completion).

Two vendor libraries use dev branch versions: pomodocs/commonmark-alert at dev-mutms-php81 and mutms/commonmark-extra at dev-main. These are custom forks maintained by the plugin author. While this works, pinning to tagged releases would improve reproducibility and make it easier to audit for security updates.

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