Add server-side redirect for /dashboard to learn.mit.edu#3356
Conversation
OpenAPI ChangesShow/hide ## Changes for v0.yaml:Unexpected changes? Ensure your branch is up-to-date with |
| """ | ||
| if request.user.is_authenticated: | ||
| global_id = request.user.global_id | ||
| if global_id and is_enabled( |
There was a problem hiding this comment.
I am a little concerned that the is_enabled call will block the dashboard response if posthog has another outage / service degradation, so opened
Adds a Django-level redirect at /dashboard so authenticated users with the PostHog flag enabled are sent to learn.mit.edu/dashboard before the legacy JS bundle is downloaded and executed. The existing client-side redirect is retained to cover in-SPA navigation. Both paths preserve query parameters in the redirect URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
38743ed to
f55f528
Compare
| redirect_url = settings.MIT_LEARN_DASHBOARD_URL | ||
| if qs := request.META.get("QUERY_STRING"): | ||
| redirect_url = f"{redirect_url}?{qs}" | ||
| return HttpResponseRedirect(redirect_url) |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 months ago
In general, the right way to fix this is to avoid building URLs with naïve string concatenation that incorporates untrusted input. Instead, parse the base URL, safely merge only those query parameters you actually want to allow, and reconstruct the URL using standard URL utilities. This both avoids malformed URLs and allows you to filter the user-provided parameters.
For this specific case in dashboard in main/views.py, we should:
- Keep the host/path fully controlled by
settings.MIT_LEARN_DASHBOARD_URL. - Parse any existing query parameters from that base URL.
- Parse the user-provided query string from
request.META["QUERY_STRING"]into key/value pairs. - Optionally filter which parameters are allowed through (at minimum, avoid raw string concatenation).
- Rebuild the final URL via
urllib.parse.urlparse/urlencode/urlunparse.
Concretely:
- Add imports for
urlparse,parse_qsl,urlencode, andurlunparsefrom Python’s standardurllib.parsemodule at the top ofmain/views.py(we may add imports, and this is a well-known standard library). - Replace lines 58–61 with logic that:
- Parses
settings.MIT_LEARN_DASHBOARD_URLusingurlparse. - Parses
request.META.get("QUERY_STRING", "")usingparse_qsl. - (Optionally) filters the resulting dict to an allowed subset; since we don’t know the rest of the app, we’ll at least ensure robust parsing and reconstruction, and not allow the query string to affect the scheme/host/path.
- Merges query parameters and reconstructs the URL with
urlunparse.
- Parses
- Keep the rest of the behavior identical: still redirects to the same domain and still preserves query parameters, but now in a controlled way.
| @@ -15,6 +15,7 @@ | ||
| from django.views.decorators.cache import never_cache | ||
| from mitol.olposthog.features import is_enabled | ||
| from rest_framework.pagination import LimitOffsetPagination | ||
| from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse | ||
|
|
||
| from main import features | ||
|
|
||
| @@ -55,9 +56,19 @@ | ||
| default=False, | ||
| opt_unique_id=global_id, | ||
| ): | ||
| redirect_url = settings.MIT_LEARN_DASHBOARD_URL | ||
| if qs := request.META.get("QUERY_STRING"): | ||
| redirect_url = f"{redirect_url}?{qs}" | ||
| base_url = settings.MIT_LEARN_DASHBOARD_URL | ||
| parsed = urlparse(base_url) | ||
| # Start with any existing query parameters on the base URL | ||
| base_query_params = dict(parse_qsl(parsed.query, keep_blank_values=True)) | ||
| # Parse query parameters supplied by the request | ||
| request_qs = request.META.get("QUERY_STRING", "") | ||
| request_query_params = dict(parse_qsl(request_qs, keep_blank_values=True)) | ||
| # Merge, giving precedence to request-supplied parameters while | ||
| # keeping scheme/host/path from the configured base URL | ||
| merged_query_params = {**base_query_params, **request_query_params} | ||
| redirect_url = urlunparse( | ||
| parsed._replace(query=urlencode(merged_query_params, doseq=True)) | ||
| ) | ||
| return HttpResponseRedirect(redirect_url) | ||
| return index(request, **kwargs) | ||
|
|
There was a problem hiding this comment.
This doesn't seem like a real concern to me. If we were redirecting to a user-provided host, that would be different. As it stands, a malicious person could give you
https://mitxonline.mit.edu/dashboard?some-malicious-stringredirecting tohttps://learn.mit.edu/dashboard?some-malicious-string
But (A) they couldve given you the second link to begin with, and (B) appending a query string to our URLs shouldn't let you do anything bad in either case.
I suspect copilot is just not distinguishing between 'user provides host' and 'user only provides query string'.
6f04f1b to
f55f528
Compare
What are the relevant tickets?
Followup to #3233, helpful for https://github.com/mitodl/hq/issues/10400
Description (What does it do?)
Adds a server-side redirect at
/dashboardfor users with theredirect-to-learn-dashboardPostHog feature flag enabled.Also forwards query params, which will likely be useful for
Previously, the redirect would only happen after being routed to legacy frontend, downloading bundle, checking feature flag, etc. Now the redirect happens on the Django server in almost all cases.
The client-side redirect in
DashboardPageis kept as-is to handle in-SPA navigation (e.g. clicking the Dashboard link from the homepage,where React Router handles the transition without a full page load andDjango never sees the request). This shouldn't actually be relevant once we switch the flag on, but could be, if there's a stray link to the dashboard in the ecommerce cart or something.
Both the server-side and client-side redirects now forward query parameters to the destination URL.
How can this be tested?
POSTHOG_PROJECT_API_KEY=your-key-hereandPOSTHOG_ENABLED=True.MIT_LEARN_DASHBOARD_URL=local-learn-url. (You can leave learn url unset, you'll just get sent to prod instead.) Create a flag calledredirect-to-learn-dashboard. Assuming you have two users A and B:distinct_id = global_uuid(see current_user api response)/dashboard/— you should be redirected toMIT_LEARN_DASHBOARD_URLwithout the legacy dashboard loading./dashboard/?foo=bar— the redirect should include?foo=bar/dashboard/should load normallyChecklist