Quiz Analytics Dashboard (Fictitious)
local_fakequizanalytics
This is a fictitious plugin used to demonstrate what an MDL Shield security review looks like. This plugin does not exist.
This plugin provides quiz analytics functionality with generally solid code structure. However, the security review identified several areas of concern, primarily around SQL query construction and output encoding. The most critical issues involve potential SQL injection vulnerabilities in the reporting module and missing capability checks on AJAX endpoints. The codebase follows Moodle coding standards reasonably well, but would benefit from consistent use of the Moodle database API and output functions.
Findings
User-supplied filter parameters are concatenated directly into SQL query without proper sanitization or use of bound parameters.
classes/report/analytics_report.php:142$sql = "SELECT * FROM {quiz_attempts}
WHERE userid = " . $userid . "
AND quiz IN (" . $quizids . ")";
$results = $DB->get_records_sql($sql);$sql = "SELECT * FROM {quiz_attempts}
WHERE userid = :userid
AND quiz IN ($quizids)";
$params = ['userid' => $userid];
// Use $DB->get_in_or_equal() for quiz IDs
list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
$sql = "SELECT * FROM {quiz_attempts} WHERE userid = :userid AND quiz $insql";
$results = $DB->get_records_sql($sql, array_merge($params, $inparams));Confirmed critical vulnerability. This is exploitable and must be fixed before release. The suggested fix correctly uses parameterized queries.
The AJAX endpoint for fetching student data does not verify the user has the required capability before returning sensitive information.
ajax/get_student_data.php:28require_login();
$userid = required_param('userid', PARAM_INT);
$data = get_student_analytics($userid);
echo json_encode($data);require_login();
$userid = required_param('userid', PARAM_INT);
$context = context_course::instance($courseid);
require_capability('local/quizanalytics:viewstudentdata', $context);
$data = get_student_analytics($userid);
echo json_encode($data);Verified. Any authenticated user could access any student's analytics data. This is a significant privacy issue.
User-supplied quiz names are output without proper encoding, allowing potential XSS attacks.
classes/output/dashboard_renderer.php:89$html .= '<h3>' . $quiz->name . '</h3>';
$html .= '<p>' . $quiz->intro . '</p>';$html .= '<h3>' . format_string($quiz->name) . '</h3>';
$html .= '<p>' . format_text($quiz->intro, FORMAT_HTML) . '</p>';Confirmed. While quiz names are typically set by teachers, this should still use proper output encoding for defense in depth.
Form submission handler does not validate the sesskey, potentially allowing CSRF attacks.
settings_save.php:15$action = optional_param('action', '', PARAM_ALPHA);
if ($action === 'save') {
save_plugin_settings($data);
redirect($returnurl);
}$action = optional_param('action', '', PARAM_ALPHA);
if ($action === 'save') {
require_sesskey();
save_plugin_settings($data);
redirect($returnurl);
}Confirmed. Standard Moodle CSRF protection pattern should be applied.
File export functionality does not properly sanitize the filename parameter.
export.php:52$filename = required_param('filename', PARAM_TEXT);
$filepath = $CFG->tempdir . '/' . $filename;
file_put_contents($filepath, $content);$filename = required_param('filename', PARAM_FILE);
$filename = clean_filename($filename);
$filepath = $CFG->tempdir . '/' . $filename;
file_put_contents($filepath, $content);Verified. Using PARAM_FILE and clean_filename() prevents directory traversal.
Direct reference to 'mdl_' prefix instead of using Moodle's table naming conventions.
db/install.php:34$sql = "CREATE INDEX mdl_quizanalytics_idx ON mdl_local_fakequizanalytics (userid)";// Use XMLDB or $DB methods instead
$table = new xmldb_table('local_fakequizanalytics');
$index = new xmldb_index('userid_idx', XMLDB_INDEX_NOTUNIQUE, ['userid']);
$dbman->add_index($table, $index);False positive. This code is in a legacy migration script that only runs on specific older installations. The current install.xml handles this correctly. No action needed.
Using deprecated get_context_instance() function instead of context_course::instance().
lib.php:78$context = get_context_instance(CONTEXT_COURSE, $courseid);$context = context_course::instance($courseid);Confirmed. This deprecated function was removed in Moodle 4.0. Must be updated for compatibility.
HTML generation in PHP could be moved to Mustache templates for better separation of concerns.
classes/output/dashboard_renderer.php:45$html = '<div class="analytics-card">';
$html .= '<h4>' . format_string($title) . '</h4>';
$html .= '<div class="stats">' . $stats . '</div>';
$html .= '</div>';
return $html;// In templates/analytics_card.mustache:
// <div class="analytics-card">
// <h4>{{title}}</h4>
// <div class="stats">{{{stats}}}</div>
// </div>
return $this->render_from_template('local_fakequizanalytics/analytics_card', [
'title' => format_string($title),
'stats' => $stats,
]);Good suggestion for maintainability, but not a security issue. Consider for future refactoring.
- The plugin would benefit from adding PHPUnit tests, particularly for the analytics calculation functions.
- Consider implementing caching for expensive database queries in the dashboard rendering.
- Documentation for capability requirements would help administrators understand the access control model.
The report above is entirely fictitious and for demonstration purposes only. No real plugin was reviewed.
Ready to secure your plugin?
Get the same thorough analysis for your Moodle plugin.