xblock-lti-consumer contain Missing Authorization in Grade Pass Back Implementation
Description
LTI Consumer XBlock 7.0.0–7.2.1 lacks authorization checks, allowing any integrated LTI tool to forge grade submissions for arbitrary XBlocks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
LTI Consumer XBlock 7.0.0–7.2.1 lacks authorization checks, allowing any integrated LTI tool to forge grade submissions for arbitrary XBlocks.
Vulnerability
Overview
The LTI Consumer XBlock (versions 7.0.0 through 7.2.1) implements the consumer side of the LTI specification for the Open edX platform [1]. A missing authorization vulnerability exists in the score submission logic: the code that uploads grades to the LMS reads the resource_link_id field from the LTI line item to determine which XBlock to credit. Because the LTI tool can supply any value for resource_link_id, a malicious LTI tool can submit scores for any LTI XBlock on the platform, bypassing intended access controls [2].
Attack
Vector and Prerequisites
An attacker must control or operate an LTI tool that is already integrated into the Open edX instance. No additional authentication or privileges are required beyond those granted to the LTI tool. The attacker only needs to know or guess the block location (the resource_link_id) of the target LTI XBlock. The LTI tool submits a grade for that arbitrary resource_link_id, and the system accepts it without verifying that the tool is authorized to modify grades for that block [2].
Impact
Successful exploitation results in a loss of integrity for LTI XBlock grades. An attacker can arbitrarily alter the scores recorded in the LMS for any LTI-integrated course component, potentially affecting student grades or course outcomes [2]. The fix, introduced in version 7.2.2, adds a check that compares the submitted resource_link_id against the block's actual configuration location field, rejecting mismatched submissions [3].
Mitigation
All users are advised to upgrade to LTI Consumer XBlock version 7.2.2 or later. No workarounds exist for earlier versions [2]. The patch was released on 2023-01-12 and is available in the project's repository [3].
AI Insight generated on May 20, 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 |
|---|---|---|
lti-consumer-xblockPyPI | >= 7.0.0, < 7.2.2 | 7.2.2 |
Affected products
2- Range: >= 7.0.0, < 7.2.2
Patches
1252f94bd182cMerge pull request from GHSA-7j9p-67mm-5g87
4 files changed · +126 −4
CHANGELOG.rst+4 −0 modified@@ -16,6 +16,10 @@ Please See the [releases tab](https://github.com/openedx/xblock-lti-consumer/rel Unreleased ~~~~~~~~~~ +7.2.2 - 2023-01-12 +------------------ +* Fixes LTI 1.3 grade injection vulnerability that allowed LTI integrations to modify scores for any block. + 7.2.1 - 2023-01-10 ------------------ * Adds support for LTI_BASE and LTI_API_BASE Django settings to allow URL configuration independent of LMS settings.
lti_consumer/__init__.py+1 −1 modified@@ -4,4 +4,4 @@ from .apps import LTIConsumerApp from .lti_xblock import LtiConsumerXBlock -__version__ = '7.2.1' +__version__ = '7.2.2'
lti_consumer/signals/signals.py+21 −3 modified@@ -22,16 +22,34 @@ def publish_grade_on_score_update(sender, instance, **kwargs): # pylint: disabl in the LMS. Trying to trigger this signal from Studio (from the Django-admin interface, for example) throw an exception. """ + line_item = instance.line_item + lti_config = line_item.lti_configuration + + # Only save score if the `line_item.resource_link_id` is the same as + # `lti_configuration.location` to prevent LTI tools to alter grades they don't + # have permissions to. + # TODO: This security mechanism will need to be reworked once we enable LTI 1.3 + # reusability to allow one configuration to save scores on multiple placements, + # but still locking down access to the items that are using the LTI configurtion. + if line_item.resource_link_id != lti_config.location: + log.warning( + "LTI tool tried publishing score %r to block %s (outside allowed scope of: %s).", + instance, + line_item.resource_link_id, + lti_config.location, + ) + return + # Before starting to publish grades to the LMS, check that: # 1. The grade being submitted in the final one - `FullyGraded` # 2. This LineItem is linked to a LMS grade - the `LtiResouceLinkId` field is set # 3. There's a valid grade in this score - `scoreGiven` is set if instance.grading_progress == LtiAgsScore.FULLY_GRADED \ - and instance.line_item.resource_link_id \ + and line_item.resource_link_id \ and instance.score_given: try: # Load block using LMS APIs and check if the block is graded and still accept grades. - block = compat.load_block_as_user(instance.line_item.resource_link_id) + block = compat.load_block_as_user(line_item.resource_link_id) if block.has_score and (not block.is_past_due() or block.accept_grades_past_due): # Map external ID to platform user user = compat.get_user_from_external_user_id(instance.user_id) @@ -58,7 +76,7 @@ def publish_grade_on_score_update(sender, instance, **kwargs): # pylint: disabl log.exception( "Error while publishing score %r to block %s to LMS: %s", instance, - instance.line_item.resource_link_id, + line_item.resource_link_id, exc, ) raise exc
lti_consumer/tests/unit/test_signals.py+100 −0 added@@ -0,0 +1,100 @@ +""" +Tests for LTI Advantage Assignments and Grades Service views. +""" +from datetime import datetime +from unittest.mock import patch, Mock + +from django.test import TestCase +from opaque_keys.edx.keys import UsageKey + +from lti_consumer.models import LtiConfiguration, LtiAgsLineItem, LtiAgsScore + + +class PublishGradeOnScoreUpdateTest(TestCase): + """ + Test the `publish_grade_on_score_update` signal. + """ + + def setUp(self): + """ + Set up resources for signal testing. + """ + self.location = UsageKey.from_string( + "block-v1:course+test+2020+type@problem+block@test" + ) + + # Create configuration + self.lti_config = LtiConfiguration.objects.create( + location=self.location, + version=LtiConfiguration.LTI_1P3, + ) + + # Patch internal method to avoid calls to modulestore + self._block_mock = Mock() + compat_mock = patch("lti_consumer.signals.signals.compat") + self.addCleanup(compat_mock.stop) + self._compat_mock = compat_mock.start() + self._compat_mock.get_user_from_external_user_id.return_value = Mock() + self._compat_mock.load_block_as_user.return_value = self._block_mock + + def test_grade_publish_not_done_when_wrong_line_item(self): + """ + Test grade publish after for a different UsageKey than set on + `lti_config.location`. + """ + # Create LineItem with `resource_link_id` != `lti_config.id` + line_item = LtiAgsLineItem.objects.create( + lti_configuration=self.lti_config, + resource_id="test", + resource_link_id=UsageKey.from_string( + "block-v1:course+test+2020+type@problem+block@different" + ), + label="test label", + score_maximum=100 + ) + + # Save score and check that LMS method wasn't called. + LtiAgsScore.objects.create( + line_item=line_item, + score_given=1, + score_maximum=1, + activity_progress=LtiAgsScore.COMPLETED, + grading_progress=LtiAgsScore.FULLY_GRADED, + user_id="test", + timestamp=datetime.now(), + ) + + # Check that methods to save grades are not called + self._block_mock.set_user_module_score.assert_not_called() + self._compat_mock.get_user_from_external_user_id.assert_not_called() + self._compat_mock.load_block_as_user.assert_not_called() + + def test_grade_publish(self): + """ + Test grade publish after if the UsageKey is equal to + the one on `lti_config.location`. + """ + # Create LineItem with `resource_link_id` != `lti_config.id` + line_item = LtiAgsLineItem.objects.create( + lti_configuration=self.lti_config, + resource_id="test", + resource_link_id=self.location, + label="test label", + score_maximum=100 + ) + + # Save score and check that LMS method wasn't called. + LtiAgsScore.objects.create( + line_item=line_item, + score_given=1, + score_maximum=1, + activity_progress=LtiAgsScore.COMPLETED, + grading_progress=LtiAgsScore.FULLY_GRADED, + user_id="test", + timestamp=datetime.now(), + ) + + # Check that methods to save grades are called + self._block_mock.set_user_module_score.assert_called_once() + self._compat_mock.get_user_from_external_user_id.assert_called_once() + self._compat_mock.load_block_as_user.assert_called_once()
Vulnerability mechanics
Root cause
"Missing authorization check in the grade publishing signal handler allowed LTI tools to submit scores for arbitrary XBlocks."
Attack vector
An attacker operating a malicious LTI tool integrated with the Open edX platform can exploit this by submitting scores with an arbitrary `resource_link_id`. Because the platform previously failed to validate this identifier against the configuration's authorized location, the tool could inject or modify grades for any LTI XBlock on the platform [patch_id=27478]. This allows unauthorized grade manipulation for any block whose location is known or guessed by the attacker.
Affected code
The vulnerability exists in `lti_consumer/signals/signals.py` within the `publish_grade_on_score_update` function. This function failed to verify that the `resource_link_id` associated with an LTI line item matched the authorized `location` defined in the `LtiConfiguration` [patch_id=27478].
What the fix does
The patch modifies `lti_consumer/signals/signals.py` to explicitly compare `line_item.resource_link_id` with `lti_config.location` before processing any grade updates. If these values do not match, the system logs a warning and aborts the operation, preventing unauthorized grade submissions to blocks outside the intended scope [patch_id=27478]. This ensures that LTI tools can only interact with the specific XBlocks they are authorized to access.
Preconditions
- authThe attacker must have an LTI tool integrated with the Open edX platform.
- inputThe attacker must know or guess the block location (resource_link_id) of the target XBlock.
Generated on May 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-7j9p-67mm-5g87ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-23611ghsaADVISORY
- github.com/openedx/xblock-lti-consumer/commit/252f94bd182cd0962af9251015930cb55ec515d7ghsaWEB
- github.com/openedx/xblock-lti-consumer/security/advisories/GHSA-7j9p-67mm-5g87ghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/lti-consumer-xblock/PYSEC-2023-21.yamlghsaWEB
News mentions
0No linked articles in our index yet.