Restriction by installed plugin
availability_plugin
Availability condition (restrict access) plugin that makes an activity or section available depending on whether a given Moodle plugin, identified by its frankenstyle name, is present on the site. It includes an autocomplete list of installed plugins for users who hold availability/plugin:addinstance.
This is a small plugin with little attack surface. It has no pages, endpoints, DB writes, file handling or outbound requests. The one piece of user input, the plugin name stored in the availability JSON, is escaped with s() when the PHP description is rendered and with Y.Escape.html() in the YUI form. It is only ever passed to core_component::get_component_directory(), which resolves names against core's own plugin type map and never builds a filesystem path from the input.
The list of installed plugins is only sent to users who pass the plugin's own capability check. A null Privacy provider is declared, and that is correct because the plugin stores no personal data. Unit and Behat tests cover the main behaviour.
The review found two things, neither of them exploitable for real harm:
- The
addinstancecapability only controls the editor UI. An editing teacher without it can still submit the condition through crafted availability JSON, and could use that to probe plugin names, which is minor information disclosure. - The condition checks whether the plugin's code is on disk, not whether the plugin is installed or enabled.
There is nothing at medium severity or above.
availability_plugin is a small restrict-access condition: two PHP classes, a null privacy provider, one capability and a YUI form module. It hides or shows course content depending on whether a named Moodle plugin is present. It has no pages, AJAX endpoints, database tables or outbound requests.
The security posture is good. The plugin name is escaped where it is rendered: with s() in get_description() and with Y.Escape.html() in the form. The only place it is used server-side is core_component::get_component_directory(), which looks names up in core's plugin map and never builds a path from them. The list of installed plugins sent to the JavaScript form is limited to users who hold availability/plugin:addinstance. By default nobody but the admin holds it.
Two things are worth knowing:
- The capability gate exists only in the editor UI. Core's availability API has no server-side hook, so an editing teacher without the capability can still save the condition through crafted availability JSON. The impact is limited to probing which plugin names exist on disk.
- The condition tests whether the plugin's code is on disk, not whether it is installed or enabled. That can differ from what the strings promise, for example when code has been uploaded but the upgrade has not run yet.
Findings
The availability/plugin:addinstance capability is checked only in frontend::allow_add() and get_javascript_init_params(), which control whether the condition appears in the Add restriction dialogue and whether the plugin list is sent to the browser. Core's availability API never calls allow_add() when saving. The availabilityconditionsjson value submitted with the activity or section form is accepted as long as each condition type exists. As a result, an editing teacher without the capability can still add a {"type":"plugin",...} condition by changing the hidden JSON field before submitting. The README notes that users without the capability can edit existing conditions, but it does not say that they can add new ones this way.
Low risk. The worst outcome is that a teacher can attach the condition to content they already control, and use it to test whether a plugin with a given name exists on the server. Teachers already see much of this through the activity chooser and the site UI. No other users' data is exposed or changed.
core_availability\frontend::include_all_javascript() calls allow_add() only to decide which buttons the YUI dialogue shows. When the edit form is submitted, the tree is parsed and saved without checking the frontend's allow_add() again.
public function allow_add($course, ?\cm_info $cm = null, ?\section_info $section = null) {
$context = $cm
? \context_module::instance($cm->id)
: \context_course::instance($course->id);
return has_capability('availability/plugin:addinstance', $context);
}
Core offers no server-side validation hook for availability conditions, so the plugin cannot fully enforce this itself. Say in the README and in the capability description that the capability controls the editor UI only, so that admins do not treat it as an access-control boundary.
is_available_for_all() treats a plugin as installed if core_component::get_component_directory() returns a directory. That function only tells you the code is present in the code tree. It does not show whether the plugin's version is registered in config_plugins (i.e. the upgrade has run) or whether the plugin is enabled, for example a disabled activity module, auth or enrolment plugin. The language strings tell students 'The plugin X is installed', which may not be accurate in those states. For names without an underscore, core's normalize_component() also resolves bare names to mod_* or core subsystems, so assign and core both count as installed.
Informational. This is a functional nuance, not a vulnerability. Content can briefly appear while a newly uploaded plugin is waiting for its upgrade, or while a plugin is disabled.
The result of this check alone decides whether content is shown to every user, because is_available() delegates to is_available_for_all().
$exists = (bool)\core_component::get_component_directory($this->pluginname);
If 'installed' should mean installed and upgraded, also check the plugin's version, e.g. get_config($this->pluginname, 'version') !== false, or use core_plugin_manager::instance()->get_plugin_info($this->pluginname) and its is_installed_and_upgraded() / is_enabled() methods. Otherwise, reword the strings to say 'present'.
The plugin-list disclosure in get_javascript_init_params() is handled carefully. Core requests init params for every plugin, whatever allow_add() returns, and the plugin returns an empty list unless the user holds the capability. tests/frontend_test.php covers this.
The capability is declared with contextlevel => CONTEXT_MODULE but is also checked in course context when sections are edited. This works because has_capability() resolves through the context hierarchy. The context level only affects where the capability appears in the role override UI.