Restriction by installed plugin
availability_plugin
Availability condition (availability_plugin) that restricts activities, resources and sections depending on whether a given Moodle plugin (frankenstyle name) is installed on the site. Offering the condition in the editor is gated by the availability/plugin:addinstance capability.
No security vulnerabilities were found. Everything I checked was handled correctly:
- The user-supplied plugin name is escaped with
s()before it goes into the HTML description string, and the YUI form escapes both the saved value and the datalist options withY.Escape.html(). - The installed-plugin lookup goes through
core_componentarray lookups and the parameterisedget_config(), so there is no filesystem or SQL exposure. - The list of installed plugins is only sent to users who hold the add capability.
The only finding is low severity. The condition constructor does not check the type of ->pluginname the way core conditions do. A crafted form submission can make it fail, but only for the user who submits it. The plugin has a correct null privacy provider, unit tests and Behat tests, and the README is open about the capability being a UI-only control.
availability_plugin is a small availability condition: about 270 lines of PHP across three classes, plus one YUI form module. It makes content available depending on whether a named plugin is installed. It stores no data of its own beyond the condition JSON that core saves, and its null privacy provider is correct.
The security posture is good. The plugin name comes from the user, and it is escaped with s() before it is placed into the HTML language string. The JavaScript form escapes every value it puts into markup. Installation checks rely only on in-memory core_component lookups and get_config(). The autocomplete list of installed plugins is withheld from users who lack availability/plugin:addinstance.
There is one low code-quality finding. The constructor does not validate the type of ->pluginname, which core's own conditions do. A tampered submission can therefore cause a fatal error, but only for the person who submits it. The README already explains that core does not enforce the add capability when the form is saved. That is a framework limitation, and it is noted below rather than raised as a finding.
Findings
The constructor casts $structure->pluginname to string without first checking that it is a string. Core conditions (for example availability_profile) check each structure field and throw a coding_exception when it is invalid, and core's tree relies on that to report malformed JSON cleanly. Here, if a crafted JSON object puts an object in pluginname, the cast throws a PHP Error (not a coding_exception) during form validation. If it puts an array there, PHP emits an Array to string conversion warning and the condition is stored as array. Normal use through the YUI form always sends a string, so only tampered submissions hit this.
Low risk. The failure only affects the user who submits the tampered form, and that user must already be able to edit the activity or section. No data belonging to other users is affected, and nothing exploitable is stored.
\core_availability\tree::__construct() builds each child with new $classname($child) directly from the submitted availabilityconditionsjson. It expects conditions to throw coding_exception when their structure is invalid, as availability/condition/profile/classes/condition.php does.
public function __construct($structure) {
$this->pluginname = isset($structure->pluginname) && $structure->pluginname !== ''
? strtolower(trim((string)$structure->pluginname))
: '';
}
public function __construct($structure) {
if (isset($structure->pluginname) && !is_string($structure->pluginname)) {
throw new \coding_exception('Invalid ->pluginname for plugin condition');
}
$this->pluginname = isset($structure->pluginname) ? strtolower(trim($structure->pluginname)) : '';
}
Add capability is a UI-only control. Core's \core_availability\frontend::include_all_javascript() calls allow_add() only to decide which buttons appear in the restriction dialogue. Core does not check it again when the module or section form is saved. A user who can edit an activity can therefore still add this condition by hand-crafting availabilityconditionsjson, and could use it to test whether a plugin with a given name is installed. The README states this limitation explicitly, and the impact is minor information disclosure to users who already edit course content, so it is not raised as a finding.
Build artefacts match the source. The YUI -min.js and -debug.js builds in yui/build/ correspond to yui/src/form/js/form.js. That includes the escaping of the saved value and of the datalist options.
Installed-state logic matches between the form and the condition. The frontend offers plugins that have a rootdir and a non-null versiondb. The condition evaluates core_component::get_component_directory() together with get_config($component, 'version'). These two checks match closely, so the autocomplete never suggests a plugin that the condition would treat as missing.