From cbccb4fe6410894d39afabb86bd95c964fe20cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Thu, 1 Oct 2026 10:07:35 +0200 Subject: [PATCH] fix: Security-Befunde aus dem Framework-Review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lti: Reflected XSS in lti_login — die 400er-Antworten spiegelten iss/client_id aus GET-Parametern als text/html; jetzt text/plain (lti/v0.1.4, Regressionstest mit " + response = self.client.get( + "/lti/login/", + { + "iss": payload, + "client_id": CLIENT_ID, + "login_hint": "user-1", + "target_link_uri": TOOL_LAUNCH, + }, + ) + self.assertEqual(response.status_code, 400) + self.assertTrue(response["Content-Type"].startswith("text/plain")) + for response in ( + self.client.post("/lti/login/", {"iss": ISSUER}), + self.client.post( + "/lti/login/", + {"iss": ISSUER, "client_id": CLIENT_ID, "target_link_uri": TOOL_LAUNCH}, + ), + ): + self.assertEqual(response.status_code, 400) + self.assertTrue(response["Content-Type"].startswith("text/plain")) + class HandshakeTests(LtiTestCase): def test_instructor_launch_provisions_user(self): diff --git a/packages/django/basicbar-lti/basicbar_lti/views.py b/packages/django/basicbar-lti/basicbar_lti/views.py index e8c12c2..1477b36 100644 --- a/packages/django/basicbar-lti/basicbar_lti/views.py +++ b/packages/django/basicbar-lti/basicbar_lti/views.py @@ -61,15 +61,15 @@ def lti_login(request): """ target = _login_param(request, "target_link_uri") if not target: - return HttpResponse("Missing target_link_uri", status=400) + return _bad_request("Missing target_link_uri") iss = _login_param(request, "iss") login_hint = _login_param(request, "login_hint") client_id = _login_param(request, "client_id") if not iss: - return HttpResponse("Missing iss", status=400) + return _bad_request("Missing iss") if not login_hint: - return HttpResponse("Missing login_hint", status=400) + return _bad_request("Missing login_hint") tool_conf = build_tool_conf() if _find_registration(tool_conf, iss, client_id) is None: @@ -77,12 +77,11 @@ def lti_login(request): "LTI login: no platform registered for issuer=%r client_id=%r", iss, client_id, ) - return HttpResponse( + return _bad_request( f"No LTI platform registered for issuer={iss!r} " f"client_id={client_id!r}. Check the platform registration " f"(issuer and client_id must match exactly, including any " - f"trailing slash).", - status=400, + f"trailing slash)." ) try: @@ -95,7 +94,14 @@ def lti_login(request): "LTI login init failed (issuer=%r client_id=%r): %s", iss, client_id, exc, ) - return HttpResponse(f"LTI login failed: {exc}", status=400) + return _bad_request(f"LTI login failed: {exc}") + + +def _bad_request(message): + """A 400 that echoes request parameters for the operator — as plain text, + never HTML: the endpoint is reachable via GET, so an HTML body would be a + reflected-XSS vector on the tool's origin.""" + return HttpResponse(message, status=400, content_type="text/plain; charset=utf-8") def lti_jwks(request): diff --git a/packages/django/basicbar-lti/pyproject.toml b/packages/django/basicbar-lti/pyproject.toml index 2ff8ea9..7251a22 100644 --- a/packages/django/basicbar-lti/pyproject.toml +++ b/packages/django/basicbar-lti/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basicbar-lti" -version = "0.1.3" +version = "0.1.4" description = "LTI-1.3-Fundament der virtUOS -bar-Tools: Plattform-Registrierung, Tool-Key, OIDC-Initiation, JWKS, Nutzer-Provisionierung, Frame-Ancestors-Middleware" readme = "README.md" requires-python = ">=3.12" diff --git a/template/project/backend/config/settings.py.jinja b/template/project/backend/config/settings.py.jinja index e92a309..a688229 100644 --- a/template/project/backend/config/settings.py.jinja +++ b/template/project/backend/config/settings.py.jinja @@ -185,6 +185,12 @@ CSRF_TRUSTED_ORIGINS = _env_list( ) REST_FRAMEWORK = { + # Session auth only. DRF's default also enables BasicAuthentication, which + # would expose every endpoint to password guessing against the break-glass + # superuser (ModelBackend) — without rate limiting and bypassing CSRF. + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], "DEFAULT_PAGINATION_CLASS": "common.pagination.StandardPagination", "PAGE_SIZE": 25, }