CVE-2014-9060
Description
Moodle LTI module mishandles return URL parameters, enabling arbitrary message generation via crafted URLs.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Moodle LTI module mishandles return URL parameters, enabling arbitrary message generation via crafted URLs.
Vulnerability
The LTI (Learning Tools Interoperability) module in Moodle through version 2.4.11, 2.5.x before 2.5.9, 2.6.x before 2.6.6, and 2.7.x before 2.7.3 does not properly restrict the parameters used in a return URL [1][2]. Specifically, in mod/lti/locallib.php, the return URL parameters included lti_errormsg and lti_msg read with PARAM_RAW in return.php, which allowed arbitrary text to be injected [4]. The vulnerability is triggered when a user interacts with the LTI launch and is redirected to the return script with a modified URL containing malicious message parameters [2][4].
Exploitation
An attacker can craft a malicious URL that points to a Moodle site's LTI return endpoint (/mod/lti/return.php) with modified lti_errormsg or lti_msg parameters. The attack requires no authentication if the LTI module is accessible, but typically relies on convincing a user to click the crafted link or visit a page that triggers the redirect. The parameters were originally defined as PARAM_RAW, meaning no sanitization was applied, allowing arbitrary text to be passed [4]. The attacker does not need any special privileges; any remote user can craft the URL [2].
Impact
Successful exploitation allows the attacker to generate arbitrary messages that are displayed to the user on the LTI return page. The message is output with htmlspecialchars() in the vulnerable code [4], which prevents direct HTML injection but still permits display of arbitrary text that could include misleading or phishing content [2]. The impact is primarily informational; the attacker cannot execute scripts or modify data directly via this vulnerability, but could use crafted messages to trick users into performing actions or revealing credentials.
Mitigation
Moodle fixed this issue in versions 2.5.9, 2.6.6, 2.7.3, and 2.8 [1][2]. The fix, introduced in commit edc89df [4], changed the parameter types from PARAM_RAW to PARAM_TEXT and added sesskey validation in the return URL to prevent unauthorized use. Users should upgrade to the fixed versions immediately. No workaround is provided for unsupported versions (pre-2.5).
AI Insight generated on May 23, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
moodle/moodlePackagist | < 2.5.9 | 2.5.9 |
moodle/moodlePackagist | >= 2.6.0, < 2.6.6 | 2.6.6 |
moodle/moodlePackagist | >= 2.7.0, < 2.7.3 | 2.7.3 |
Affected products
20cpe:2.3:a:moodle:moodle:*:*:*:*:*:*:*:*+ 18 more
- cpe:2.3:a:moodle:moodle:*:*:*:*:*:*:*:*range: <=2.4.11
- cpe:2.3:a:moodle:moodle:2.5.0:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.1:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.2:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.3:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.4:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.5:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.6:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.7:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.5.8:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.0:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.2:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.3:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.4:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.6.5:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.7.0:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.7.1:*:*:*:*:*:*:*
- cpe:2.3:a:moodle:moodle:2.7.2:*:*:*:*:*:*:*
Patches
4339c6eca3c88MDL-47927 LTI: Use PARAM_TEXT and p() for returned messages and errors
2 files changed · +9 −5
mod/lti/locallib.php+4 −1 modified@@ -154,7 +154,10 @@ function lti_view($instance) { $requestparams = lti_build_request($instance, $typeconfig, $course); $launchcontainer = lti_get_launch_container($instance, $typeconfig); - $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id); + $returnurlparams = array('course' => $course->id, + 'launch_container' => $launchcontainer, + 'instanceid' => $instance->id, + 'sesskey' => sesskey()); if ( $orgid ) { $requestparams["tool_consumer_instance_guid"] = $orgid;
mod/lti/return.php+5 −4 modified@@ -31,8 +31,8 @@ $courseid = required_param('course', PARAM_INT); $instanceid = optional_param('instanceid', 0, PARAM_INT); -$errormsg = optional_param('lti_errormsg', '', PARAM_RAW); -$msg = optional_param('lti_msg', '', PARAM_RAW); +$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); +$msg = optional_param('lti_msg', '', PARAM_TEXT); $unsigned = optional_param('unsigned', '0', PARAM_INT); $launchcontainer = optional_param('launch_container', LTI_LAUNCH_CONTAINER_WINDOW, PARAM_INT); @@ -48,6 +48,7 @@ require_login($course); +require_sesskey(); if (!empty($errormsg) || !empty($msg)) { $url = new moodle_url('/mod/lti/return.php', array('course' => $courseid)); @@ -73,7 +74,7 @@ if (!empty($errormsg)) { echo get_string('lti_launch_error', 'lti'); - echo htmlspecialchars($errormsg); + p($errormsg); if ($unsigned == 1) { @@ -100,7 +101,7 @@ echo $OUTPUT->footer(); } else if (!empty($msg)) { - echo htmlspecialchars($msg); + p($msg); echo $OUTPUT->footer();
edc89dfecb3fMDL-47927 LTI: Use PARAM_TEXT and p() for returned messages and errors
2 files changed · +9 −5
mod/lti/locallib.php+4 −1 modified@@ -179,7 +179,10 @@ function lti_view($instance) { $instance->instructorcustomparameters, $islti2)); $launchcontainer = lti_get_launch_container($instance, $typeconfig); - $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id); + $returnurlparams = array('course' => $course->id, + 'launch_container' => $launchcontainer, + 'instanceid' => $instance->id, + 'sesskey' => sesskey()); // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
mod/lti/return.php+5 −4 modified@@ -30,8 +30,8 @@ $courseid = required_param('course', PARAM_INT); $instanceid = optional_param('instanceid', 0, PARAM_INT); -$errormsg = optional_param('lti_errormsg', '', PARAM_RAW); -$msg = optional_param('lti_msg', '', PARAM_RAW); +$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); +$msg = optional_param('lti_msg', '', PARAM_TEXT); $unsigned = optional_param('unsigned', '0', PARAM_INT); $launchcontainer = optional_param('launch_container', LTI_LAUNCH_CONTAINER_WINDOW, PARAM_INT); @@ -47,6 +47,7 @@ require_login($course); +require_sesskey(); if (!empty($errormsg) || !empty($msg)) { $url = new moodle_url('/mod/lti/return.php', array('course' => $courseid)); @@ -72,7 +73,7 @@ if (!empty($errormsg)) { echo get_string('lti_launch_error', 'lti'); - echo htmlspecialchars($errormsg); + p($errormsg); if ($unsigned == 1) { @@ -99,7 +100,7 @@ echo $OUTPUT->footer(); } else if (!empty($msg)) { - echo htmlspecialchars($msg); + p($msg); echo $OUTPUT->footer();
15bde5352bd4MDL-47927 LTI: Use PARAM_TEXT and p() for returned messages and errors
2 files changed · +7 −3
mod/lti/locallib.php+4 −1 modified@@ -154,7 +154,10 @@ function lti_view($instance) { $requestparams = lti_build_request($instance, $typeconfig, $course); $launchcontainer = lti_get_launch_container($instance, $typeconfig); - $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id); + $returnurlparams = array('course' => $course->id, + 'launch_container' => $launchcontainer, + 'instanceid' => $instance->id, + 'sesskey' => sesskey()); if ( $orgid ) { $requestparams["tool_consumer_instance_guid"] = $orgid;
mod/lti/return.php+3 −2 modified@@ -31,14 +31,15 @@ $courseid = required_param('course', PARAM_INT); $instanceid = required_param('instanceid', PARAM_INT); -$errormsg = optional_param('lti_errormsg', '', PARAM_RAW); +$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); $unsigned = optional_param('unsigned', '0', PARAM_INT); $launchcontainer = optional_param('launch_container', LTI_LAUNCH_CONTAINER_WINDOW, PARAM_INT); $course = $DB->get_record('course', array('id' => $courseid)); require_login($course); +require_sesskey(); if (!empty($errormsg)) { $url = new moodle_url('/mod/lti/return.php', array('course' => $courseid)); @@ -59,7 +60,7 @@ echo get_string('lti_launch_error', 'lti'); - echo htmlspecialchars($errormsg); + p($errormsg); if ($unsigned == 1) {
44e712e9b72aMDL-47927 LTI: Use PARAM_TEXT and p() for returned messages and errors
2 files changed · +9 −5
mod/lti/locallib.php+4 −1 modified@@ -153,7 +153,10 @@ function lti_view($instance) { $requestparams = lti_build_request($instance, $typeconfig, $course); $launchcontainer = lti_get_launch_container($instance, $typeconfig); - $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id); + $returnurlparams = array('course' => $course->id, + 'launch_container' => $launchcontainer, + 'instanceid' => $instance->id, + 'sesskey' => sesskey()); if ( $orgid ) { $requestparams["tool_consumer_instance_guid"] = $orgid;
mod/lti/return.php+5 −4 modified@@ -30,8 +30,8 @@ $courseid = required_param('course', PARAM_INT); $instanceid = optional_param('instanceid', 0, PARAM_INT); -$errormsg = optional_param('lti_errormsg', '', PARAM_RAW); -$msg = optional_param('lti_msg', '', PARAM_RAW); +$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); +$msg = optional_param('lti_msg', '', PARAM_TEXT); $unsigned = optional_param('unsigned', '0', PARAM_INT); $launchcontainer = optional_param('launch_container', LTI_LAUNCH_CONTAINER_WINDOW, PARAM_INT); @@ -47,6 +47,7 @@ require_login($course); +require_sesskey(); if (!empty($errormsg) || !empty($msg)) { $url = new moodle_url('/mod/lti/return.php', array('course' => $courseid)); @@ -72,7 +73,7 @@ if (!empty($errormsg)) { echo get_string('lti_launch_error', 'lti'); - echo htmlspecialchars($errormsg); + p($errormsg); if ($unsigned == 1) { @@ -99,7 +100,7 @@ echo $OUTPUT->footer(); } else if (!empty($msg)) { - echo htmlspecialchars($msg); + p($msg); echo $OUTPUT->footer();
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-c87j-9rrq-h3j8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-9060ghsaADVISORY
- openwall.com/lists/oss-security/2014/11/17/11nvdWEB
- github.com/moodle/moodle/commit/15bde5352bd4bdb54105c0fdfd956c9ca420e4c6ghsaWEB
- github.com/moodle/moodle/commit/339c6eca3c881742178637cb41cc7ebbe4a3b6b0ghsaWEB
- github.com/moodle/moodle/commit/44e712e9b72a30c6bc01112040854e91f5758605ghsaWEB
- github.com/moodle/moodle/commit/edc89dfecb3f6891cea019baf2aecce51b3de41aghsaWEB
- moodle.org/mod/forum/discuss.phpnvdWEB
- web.archive.org/web/20150914064838/http://www.securitytracker.com/id/1031215ghsaWEB
- www.securitytracker.com/id/1031215nvd
News mentions
0No linked articles in our index yet.