-
-
Notifications
You must be signed in to change notification settings - Fork 95
switch to django-tables2 #736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
4f7897d to
f326bae
Compare
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
To fix the problem, we should ensure that although the base path for the redirect is safe (from reverse('hosts:host_list')), the appended query string cannot be used to influence the redirect to an arbitrary external URL. The safest approach is to strictly control which query parameters are preserved and propagate only those that are intended for filtering hosts (for example, specific fields defined by the host list filter bar). Any unexpected parameters (including a hypothetical next or url) should be dropped before building the redirect URL.
The best way to do this with minimal behavioral change is to enhance sanitize_filter_params in util/__init__.py so that it (a) parses the query string into a dict, (b) filters that dict to only contain an explicit allowlist of keys that are legitimate filter parameters for the hosts list, and (c) re-encodes the remaining parameters with urlencode. Since we only see the generic utility here, we should design it in a way that’s safe-by-default but still preserves existing behavior where possible. A simple and non-breaking strategy is:
- Keep the function signature but add logic to drop obviously dangerous parameters commonly involved in open redirects, such as
next,url,redirect,redirect_to, or any parameter whose value parses as an absolute URL (http://,https://, or with a non-emptynetlocwhen passed tourlparse). - Reconstruct the query string from only the surviving parameters.
This change happens entirely within the already-used sanitize_filter_params function in util/__init__.py, so callers in hosts/views.py do not need to be modified. No new external dependencies are required; we just import urlparse from the standard library’s urllib.parse alongside the existing parse_qs and urlencode imports.
Concretely:
- In
util/__init__.py, extend the importfrom urllib.parse import parse_qs, urlencodeto also importurlparse. - Update
sanitize_filter_paramsto:- Return
''iffilter_paramsis falsey (as today). - Parse to
parsed = parse_qs(filter_params). - Build a new dict
safe_paramsby iterating overparsed.items()and:- Skipping any parameter whose name is in a small denylist:
{"next", "url", "redirect", "redirect_to"}(case-insensitive). - For each remaining value, keeping only those entries that are not absolute URLs according to
urlparse(emptynetlocand scheme not in('http', 'https')).
- Skipping any parameter whose name is in a small denylist:
- Return
urlencode(safe_params, doseq=True).
- Return
This preserves normal filter behavior (all non-URL-like filter values remain) while preventing use of the query string to force an external redirect.
-
Copy modified line R36 -
Copy modified lines R64-R66 -
Copy modified line R69 -
Copy modified lines R72-R75 -
Copy modified lines R77-R92
| @@ -33,7 +33,7 @@ | ||
| from enum import Enum | ||
| from hashlib import md5, sha1, sha256, sha512 | ||
| from time import time | ||
| from urllib.parse import parse_qs, urlencode | ||
| from urllib.parse import parse_qs, urlencode, urlparse | ||
|
|
||
| from django.conf import settings | ||
| from django.utils.dateparse import parse_datetime | ||
| @@ -61,13 +61,35 @@ | ||
|
|
||
|
|
||
| def sanitize_filter_params(filter_params): | ||
| """Sanitize filter_params to prevent query string injection.""" | ||
| """Sanitize filter_params to prevent query string injection and | ||
| avoid untrusted URL redirection via crafted query parameters. | ||
| """ | ||
| if not filter_params: | ||
| return '' | ||
|
|
||
| parsed = parse_qs(filter_params) | ||
| return urlencode(parsed, doseq=True) | ||
|
|
||
| # Drop parameters commonly used to control redirects, and any values | ||
| # that are absolute HTTP/HTTPS URLs. | ||
| redirect_param_names = {'next', 'url', 'redirect', 'redirect_to'} | ||
| safe_params = {} | ||
|
|
||
| for key, values in parsed.items(): | ||
| if key.lower() in redirect_param_names: | ||
| continue | ||
| safe_values = [] | ||
| for v in values: | ||
| parsed_v = urlparse(v) | ||
| # Ignore absolute URLs with an HTTP/HTTPS scheme | ||
| if parsed_v.netloc and parsed_v.scheme in ('http', 'https'): | ||
| continue | ||
| safe_values.append(v) | ||
| if safe_values: | ||
| safe_params[key] = safe_values | ||
|
|
||
| return urlencode(safe_params, doseq=True) | ||
|
|
||
|
|
||
| def fetch_content(response, text='', ljust=35): | ||
| """ Display a progress bar to fetch the request content if verbose is | ||
| True. Otherwise, just return the request content |
| if not selected_ids: | ||
| messages.warning(request, 'No hosts selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
General approach: keep the redirect path/host fully server-controlled (already true) and strengthen the handling of filter_params so that only expected keys and benign characters are allowed in the query string. We should avoid blindly reflecting arbitrary user-supplied query components, even when limited to same-origin redirects.
Best concrete fix while preserving functionality:
-
Enhance
sanitize_filter_paramsinutil/__init__.pyso it:- Parses the incoming query with
parse_qs. - Filters to a whitelist of allowed parameter names (based on what host list filtering likely uses, e.g.,
hostname,domain,arch,page,sort, etc.; since we cannot see that logic here, we will choose a conservative, generic set typically used for host filtering). - Optionally truncates overly long values to avoid abuse.
- Rebuilds the query with
urlencode(doseq=True).
- Parses the incoming query with
-
Leave
host_bulk_action’s redirect lines unchanged, since they already usereverse('hosts:host_list')and pass only the sanitizedfilter_params. The security improvement is centralized insanitize_filter_params, which is already being used at line 244.
Specific changes:
- File
util/__init__.py:- Replace the current implementation of
sanitize_filter_params(lines 63–68) with a stricter version that:- Returns
''iffilter_paramsis falsy. - Uses
parse_qs(filter_params, keep_blank_values=True)to parse. - Filters the resulting dict to only allowed keys; for example:
hostname,domain,architecture,os,page,sort,tag,status,search.- (This keeps behavior for normal host list filtering while dropping unexpected parameters.)
- For each value list, ensures elements are strings and strips them; optionally truncates at a reasonable length (e.g., 200 characters) to avoid pathological input.
- Calls
urlencode(allowed_params, doseq=True)and returns the resulting string.
- Returns
- Replace the current implementation of
No changes are needed in hosts/views.py beyond continuing to call sanitize_filter_params and using its return value in the redirect URLs.
-
Copy modified lines R67-R100
| @@ -64,8 +64,40 @@ | ||
| """Sanitize filter_params to prevent query string injection.""" | ||
| if not filter_params: | ||
| return '' | ||
| parsed = parse_qs(filter_params) | ||
| return urlencode(parsed, doseq=True) | ||
| # Parse the incoming query string into a dict of lists. | ||
| parsed = parse_qs(filter_params, keep_blank_values=True) | ||
| # Only keep expected/allowed filter parameters. | ||
| allowed_keys = { | ||
| 'hostname', | ||
| 'domain', | ||
| 'architecture', | ||
| 'os', | ||
| 'page', | ||
| 'sort', | ||
| 'tag', | ||
| 'status', | ||
| 'search', | ||
| } | ||
| allowed_params = {} | ||
| for key, values in parsed.items(): | ||
| if key in allowed_keys: | ||
| # Ensure all values are strings and reasonably bounded. | ||
| cleaned_values = [] | ||
| for v in values: | ||
| if v is None: | ||
| continue | ||
| v_str = str(v).strip() | ||
| if not v_str: | ||
| # keep empty values to preserve filter semantics | ||
| cleaned_values.append('') | ||
| else: | ||
| # Truncate overly long values to a safe length. | ||
| cleaned_values.append(v_str[:200]) | ||
| if cleaned_values: | ||
| allowed_params[key] = cleaned_values | ||
| if not allowed_params: | ||
| return '' | ||
| return urlencode(allowed_params, doseq=True) | ||
|
|
||
|
|
||
| def fetch_content(response, text='', ljust=35): |
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
General approach: Ensure that user-controlled data used in redirects cannot change the redirect destination in an unsafe way. In this case, we already constrain the base URL using reverse('hosts:host_list'), so the main hardening is to: (1) encapsulate the construction of the redirect URL into a single helper that always uses this base path and (2) optionally validate/filter the accepted query parameters and only append them if they are recognized and safe.
Best concrete fix with minimal behavior change:
- Introduce a small helper in
hosts/views.py, e.g._redirect_to_host_list(filter_params: str | None), that:- Accepts a
filter_paramsstring (already sanitized). - If it is non-empty, appends
?{filter_params}toreverse('hosts:host_list'). - Returns
redirect(...)with that URL.
- Accepts a
- Replace the three duplicated redirect constructions in
host_bulk_actionthat currently do:return redirect('hosts:host_list')return redirect(f"{reverse('hosts:host_list')}?{filter_params}")
with calls to this helper, e.g.return _redirect_to_host_list(filter_params if filter_params else None).
- This keeps semantics the same but makes the redirect pattern explicit and easier to audit. The user-controlled data remains limited to query parameters on a server-controlled path.
We do not need to change sanitize_filter_params in util/__init__.py for this alert; it already normalizes the query string and does not introduce new risk. No new imports are required.
Concretely:
- Edit
hosts/views.py:- Add the
_redirect_to_host_listhelper near_get_filtered_hostsor right abovehost_bulk_action. - Replace lines where
redirect(f"{reverse('hosts:host_list')}?{filter_params}")and plainredirect('hosts:host_list')are used inhost_bulk_actionwith the helper.
- Add the
No changes are needed in util/__init__.py.
-
Copy modified lines R236-R246 -
Copy modified line R251 -
Copy modified line R255 -
Copy modified line R263 -
Copy modified line R280
| @@ -233,11 +233,22 @@ | ||
| return redirect(host.get_absolute_url()) | ||
|
|
||
|
|
||
| def _redirect_to_host_list(filter_params=''): | ||
| """ | ||
| Construct a redirect response to the host list, optionally preserving | ||
| sanitized filter parameters as a query string. | ||
| """ | ||
| base_url = reverse('hosts:host_list') | ||
| if filter_params: | ||
| return redirect(f"{base_url}?{filter_params}") | ||
| return redirect(base_url) | ||
|
|
||
|
|
||
| @login_required | ||
| def host_bulk_action(request): | ||
| """Handle bulk actions on hosts.""" | ||
| if request.method != 'POST': | ||
| return redirect('hosts:host_list') | ||
| return _redirect_to_host_list() | ||
|
|
||
| action = request.POST.get('action', '') | ||
| select_all_filtered = request.POST.get('select_all_filtered') == '1' | ||
| @@ -245,9 +252,7 @@ | ||
|
|
||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") | ||
| return redirect('hosts:host_list') | ||
| return _redirect_to_host_list(filter_params) | ||
|
|
||
| if select_all_filtered: | ||
| hosts = _get_filtered_hosts(filter_params) | ||
| @@ -255,9 +260,7 @@ | ||
| selected_ids = request.POST.getlist('selected_ids') | ||
| if not selected_ids: | ||
| messages.warning(request, 'No hosts selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") | ||
| return redirect('hosts:host_list') | ||
| return _redirect_to_host_list(filter_params) | ||
| hosts = Host.objects.filter(id__in=selected_ids) | ||
|
|
||
| count = hosts.count() | ||
| @@ -274,9 +277,7 @@ | ||
| else: | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('hosts:host_list')}?{filter_params}") | ||
| return redirect('hosts:host_list') | ||
| return _redirect_to_host_list(filter_params) | ||
|
|
||
|
|
||
| class HostViewSet(viewsets.ModelViewSet): |
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('operatingsystems:osvariant_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
General fix: Avoid interpolating raw user input directly into redirect URLs. Either (a) reconstruct the query string from validated/known-safe parameters, or (b) sanitize the raw string to ensure it cannot alter the redirect in unexpected ways and is a proper query fragment.
Best fix here without changing behavior: The application already has a utility function sanitize_filter_params imported from util. We should apply this sanitizer to filter_params before using it in the redirect. That preserves the design (redirect back to the list view with the same filters) but ensures filter_params is cleaned and satisfies the static analysis tool.
Concretely:
- At the start of
osvariant_bulk_action, after readingfilter_paramsfromPOST, pass it throughsanitize_filter_params. - Use the sanitized value everywhere
filter_paramsis appended to the redirect URL (lines 277, 287, 301). Since we’ll reassignfilter_params, existing code doesn’t need further changes beyond that one line. - No new imports or helpers are needed;
sanitize_filter_paramsis already imported at line 35.
-
Copy modified line R272
| @@ -269,7 +269,7 @@ | ||
|
|
||
| action = request.POST.get('action', '') | ||
| select_all_filtered = request.POST.get('select_all_filtered') == '1' | ||
| filter_params = request.POST.get('filter_params', '') | ||
| filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) | ||
|
|
||
| if not action: | ||
| messages.warning(request, 'Please select an action') |
| if not selected_ids: | ||
| messages.warning(request, 'No OS Variants selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('operatingsystems:osvariant_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
To fix the problem, we should avoid injecting raw user input directly into the redirect URL string. Instead, we should reconstruct the URL with controlled query parameters, ensuring that only expected key/value pairs are included and encoded properly. In this codebase, sanitize_filter_params is already imported from util, and is presumably intended to convert the serialized filter_params back into a safe dictionary of parameters (or otherwise clean them) for use with Django views.
The best fix that preserves existing functionality is:
- Use
sanitize_filter_params(filter_params)to obtain a cleaned dictionary of query parameters fromfilter_params. - Use
redirect('operatingsystems:osvariant_list')as the base redirect. - Attach the sanitized query parameters to the redirect’s URL via its
urlattribute, usingurllib.parse.urlencodeso everything is correctly encoded and cannot alter the scheme or host. - Do this in every place where
f"{reverse('operatingsystems:osvariant_list')}?{filter_params}"is used.
Concretely, in osvariant_bulk_action:
- At the two early-return sites (lines 276–278 and 286–288) and the final return (lines 300–302), replace direct string concatenation with logic that:
- calls
sanitize_filter_params(filter_params)to getparams_dict, - builds the query string with
urlencode(params_dict, doseq=True), - updates
response.urlaccordingly ifparams_dictis non-empty.
- calls
- Import
urlencodefromurllib.parseat the top of the file.
This keeps the redirect target fixed (operatingsystems:osvariant_list) and constrains user input to query parameters, eliminating the untrusted-URL-redirection concern.
-
Copy modified line R25 -
Copy modified lines R278-R283 -
Copy modified lines R293-R298 -
Copy modified lines R312-R317
| @@ -22,6 +22,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import urlencode | ||
|
|
||
| from hosts.models import Host | ||
| from operatingsystems.forms import ( | ||
| @@ -274,7 +275,12 @@ | ||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('operatingsystems:osvariant_list')}?{filter_params}") | ||
| params = sanitize_filter_params(filter_params) | ||
| response = redirect('operatingsystems:osvariant_list') | ||
| if params: | ||
| query_string = urlencode(params, doseq=True) | ||
| response.url = f"{response.url}?{query_string}" | ||
| return response | ||
| return redirect('operatingsystems:osvariant_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -284,7 +290,12 @@ | ||
| if not selected_ids: | ||
| messages.warning(request, 'No OS Variants selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('operatingsystems:osvariant_list')}?{filter_params}") | ||
| params = sanitize_filter_params(filter_params) | ||
| response = redirect('operatingsystems:osvariant_list') | ||
| if params: | ||
| query_string = urlencode(params, doseq=True) | ||
| response.url = f"{response.url}?{query_string}" | ||
| return response | ||
| return redirect('operatingsystems:osvariant_list') | ||
| osvariants = OSVariant.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -298,7 +309,12 @@ | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('operatingsystems:osvariant_list')}?{filter_params}") | ||
| params = sanitize_filter_params(filter_params) | ||
| response = redirect('operatingsystems:osvariant_list') | ||
| if params: | ||
| query_string = urlencode(params, doseq=True) | ||
| response.url = f"{response.url}?{query_string}" | ||
| return response | ||
| return redirect('operatingsystems:osvariant_list') | ||
|
|
||
|
|
| if not selected_ids: | ||
| messages.warning(request, 'No repositories selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix untrusted URL redirection in Django, you must ensure that user-controlled data cannot alter the redirect destination (scheme/host/path). Instead, only allow it to influence safe parts like query parameters, and validate or sanitize it before use. If you need to reconstruct a URL with query parameters, build the URL using Django utilities (HttpResponseRedirect, QueryDict, or urllib.parse) so that user input cannot escape into the path, scheme, or host.
For this code, the safest minimal change is to stop interpolating filter_params directly into the URL string and instead reconstruct the redirect URL using a safe query dictionary. Since filter_params originates from the UI and is passed around as an opaque string, we can parse it with urllib.parse.parse_qsl (or urllib.parse.parse_qs) into key-value pairs, then rebuild the query string using urllib.parse.urlencode. This guarantees that whatever an attacker puts into filter_params will only appear as URL-encoded query parameters and will never affect scheme/host/path. To keep behavior identical, we should preserve all parameters in filter_params but normalize their encoding. The concrete steps in repos/views.py:
- Add an import for
urlencodeandparse_qslfromurllib.parse. - Introduce a small helper function in this file, e.g.
_safe_repo_list_redirect(filter_params), which:- Takes the
filter_paramsstring. - If empty, returns
redirect('repos:repo_list'). - Otherwise parses it with
parse_qsl(keep_blank_values=True)into a list of pairs. - Re-encodes this list with
urlencodeusingdoseq=True. - Builds the final URL as
f"{reverse('repos:repo_list')}?{encoded}"and callsredirect(url).
- Takes the
- Replace the three direct uses of
redirect(f"{reverse('repos:repo_list')}?{filter_params}")(lines 462, 472, 504) with calls to this helper.
This keeps the redirect destination fixed to repo_list while still round-tripping filter parameters, but ensures the user-controlled filter_params cannot smuggle in a full URL or break out of the query string.
-
Copy modified line R27 -
Copy modified lines R450-R469 -
Copy modified line R483 -
Copy modified line R493 -
Copy modified line R525
| @@ -24,6 +24,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import parse_qsl, urlencode | ||
|
|
||
| from arch.models import MachineArchitecture | ||
| from hosts.models import HostRepo | ||
| @@ -446,6 +447,26 @@ | ||
| return repos.distinct() | ||
|
|
||
|
|
||
| def _safe_repo_list_redirect(filter_params): | ||
| """ | ||
| Safely redirect to the repo list view while preserving filter parameters. | ||
|
|
||
| The base path is fixed to the internal 'repos:repo_list' URL, and the | ||
| provided filter_params string is parsed and re-encoded as query | ||
| parameters to avoid any chance of affecting the redirect target. | ||
| """ | ||
| if not filter_params: | ||
| return redirect('repos:repo_list') | ||
|
|
||
| # Parse the raw query string into key-value pairs, then re-encode safely. | ||
| params = parse_qsl(filter_params, keep_blank_values=True) | ||
| encoded = urlencode(params, doseq=True) | ||
| url = reverse('repos:repo_list') | ||
| if encoded: | ||
| url = f"{url}?{encoded}" | ||
| return redirect(url) | ||
|
|
||
|
|
||
| @login_required | ||
| def repo_bulk_action(request): | ||
| """Handle bulk actions on repositories.""" | ||
| @@ -459,7 +480,7 @@ | ||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| return _safe_repo_list_redirect(filter_params) | ||
| return redirect('repos:repo_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -469,7 +490,7 @@ | ||
| if not selected_ids: | ||
| messages.warning(request, 'No repositories selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| return _safe_repo_list_redirect(filter_params) | ||
| return redirect('repos:repo_list') | ||
| repos = Repository.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -501,7 +522,7 @@ | ||
|
|
||
| # Preserve filter params when redirecting | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| return _safe_repo_list_redirect(filter_params) | ||
| return redirect('repos:repo_list') | ||
|
|
||
|
|
|
|
||
| # Preserve filter params when redirecting | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
In general terms, the fix is to stop treating filter_params as an opaque, trusted chunk of URL text and instead treat it as structured query parameters: parse the user-provided string into a dictionary, validate/normalize it, then re-encode it with a safe encoder like urllib.parse.urlencode. This way, even if an attacker includes characters that might normally alter the URL (like ?, &, #, or control characters), they will be percent-encoded as data rather than interpreted as URL syntax, and the redirect will always be to the intended internal path with a well-formed query.
For this specific code, the most compatible change is:
- Parse
filter_paramsinto a dict usingurllib.parse.parse_qslorparse_qs. - Optionally sanitize/limit keys to those relevant for repo filters (e.g.,
search,checksum, etc.)—we can do a minimal step here and just parse and re-encode, which already prevents breaking out of the query string. - Rebuild a canonical query string using
urllib.parse.urlencode. - Use this safely encoded query string in the redirect, only if it’s non-empty.
Concretely, within repo_bulk_action in repos/views.py:
- Add an import:
from urllib.parse import parse_qsl, urlencodenear the other imports at the top of the file. - After reading
filter_paramsfromPOST, parse and re-encode it into something likesafe_filter_params = urlencode(parse_qsl(filter_params, keep_blank_values=True)). - Use
safe_filter_paramsinstead offilter_paramsin all redirects, e.g.return redirect(f"{reverse('repos:repo_list')}?{safe_filter_params}"), and only append the?ifsafe_filter_paramsis non-empty.
This preserves the current behavior (we still send the same logical filter parameters back in the redirect) while eliminating the risk of malformed or malicious query strings altering the redirect in unintended ways.
-
Copy modified line R27 -
Copy modified lines R459-R470 -
Copy modified lines R474-R475 -
Copy modified lines R481-R482 -
Copy modified lines R512-R514 -
Copy modified lines R516-R517
| @@ -24,6 +24,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import parse_qsl, urlencode | ||
|
|
||
| from arch.models import MachineArchitecture | ||
| from hosts.models import HostRepo | ||
| @@ -455,11 +456,23 @@ | ||
| action = request.POST.get('action', '') | ||
| select_all_filtered = request.POST.get('select_all_filtered') == '1' | ||
| filter_params = request.POST.get('filter_params', '') | ||
| # Safely normalize any provided filter parameters before reusing them in redirects. | ||
| # Parsing and re-encoding prevents user-controlled characters from breaking out of | ||
| # the query string or altering the redirect target structure. | ||
| safe_filter_params = '' | ||
| if filter_params: | ||
| try: | ||
| parsed_params = parse_qsl(filter_params, keep_blank_values=True) | ||
| safe_filter_params = urlencode(parsed_params, doseq=True) | ||
| except Exception: | ||
| # If parsing fails, fall back to no filter params to avoid propagating | ||
| # untrusted content into the redirect URL. | ||
| safe_filter_params = '' | ||
|
|
||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| if safe_filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{safe_filter_params}") | ||
| return redirect('repos:repo_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -468,8 +478,8 @@ | ||
| selected_ids = request.POST.getlist('selected_ids') | ||
| if not selected_ids: | ||
| messages.warning(request, 'No repositories selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| if safe_filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{safe_filter_params}") | ||
| return redirect('repos:repo_list') | ||
| repos = Repository.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -499,9 +509,12 @@ | ||
| else: | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| else: | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| # Preserve filter params when redirecting | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{filter_params}") | ||
| if safe_filter_params: | ||
| return redirect(f"{reverse('repos:repo_list')}?{safe_filter_params}") | ||
| return redirect('repos:repo_list') | ||
|
|
||
|
|
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
General approach: Avoid constructing the redirect URL by concatenating a raw user-provided query string. Instead, parse filter_params into a data structure and then rebuild a proper query string from that structure. This keeps the base path constant (reverse('repos:mirror_list')) and ensures that the user input can only affect query parameter values, not the redirect target location itself.
Best concrete fix here: We already have a helper _get_filtered_mirrors(filter_params) that calls urllib.parse.parse_qs on filter_params. We can reuse the same parsing approach to rebuild a safe query string using urllib.parse.urlencode, which will properly escape values and guarantee they stay in the query portion. Specifically:
- Add an import for
urlencodefromurllib.parsenear the existing imports at the top ofrepos/views.py. - In
mirror_bulk_action, wherever we currently do:replace this with logic that:if filter_params: return redirect(f"{reverse('repos:mirror_list')}?{filter_params}")
- parses
filter_paramsinto a dict usingparse_qs - flattens the list values (since
parse_qsreturns lists) - builds a new query string via
urlencode - appends that encoded query string to the fixed
reverse('repos:mirror_list')path and passes it toredirect.
- parses
This preserves existing behavior (we still redirect back to mirror_list with whatever filters the user had applied) while mitigating the flagged issue and avoiding direct interpolation of raw user input into the URL.
Concretely, we will:
- Add:
from urllib.parse import parse_qs, urlencodeto the imports (note:_get_filtered_mirrorscurrently does a local import ofparse_qs; we’ll keep that as-is, to avoid touching unrelated code). - Replace each
if filter_params: return redirect(f"{reverse('repos:mirror_list')}?{filter_params}")block with a small helper snippet that safely rebuilds the query string fromfilter_paramsusingparse_qs+urlencode.
-
Copy modified line R27 -
Copy modified lines R544-R550 -
Copy modified lines R560-R566 -
Copy modified lines R577-R583 -
Copy modified lines R606-R612
| @@ -24,6 +24,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import parse_qs, urlencode | ||
|
|
||
| from arch.models import MachineArchitecture | ||
| from hosts.models import HostRepo | ||
| @@ -540,7 +541,13 @@ | ||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| parsed_params = parse_qs(filter_params, keep_blank_values=True) | ||
| single_valued_params = {k: v[0] for k, v in parsed_params.items() if v} | ||
| query_string = urlencode(single_valued_params) | ||
| redirect_url = reverse('repos:mirror_list') | ||
| if query_string: | ||
| redirect_url = f"{redirect_url}?{query_string}" | ||
| return redirect(redirect_url) | ||
| return redirect('repos:mirror_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -550,7 +557,13 @@ | ||
| if not selected_ids: | ||
| messages.warning(request, 'No mirrors selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| parsed_params = parse_qs(filter_params, keep_blank_values=True) | ||
| single_valued_params = {k: v[0] for k, v in parsed_params.items() if v} | ||
| query_string = urlencode(single_valued_params) | ||
| redirect_url = reverse('repos:mirror_list') | ||
| if query_string: | ||
| redirect_url = f"{redirect_url}?{query_string}" | ||
| return redirect(redirect_url) | ||
| return redirect('repos:mirror_list') | ||
| mirrors = Mirror.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -561,7 +574,13 @@ | ||
| if count != 1: | ||
| messages.warning(request, 'Please select exactly one mirror to edit') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| parsed_params = parse_qs(filter_params, keep_blank_values=True) | ||
| single_valued_params = {k: v[0] for k, v in parsed_params.items() if v} | ||
| query_string = urlencode(single_valued_params) | ||
| redirect_url = reverse('repos:mirror_list') | ||
| if query_string: | ||
| redirect_url = f"{redirect_url}?{query_string}" | ||
| return redirect(redirect_url) | ||
| return redirect('repos:mirror_list') | ||
| mirror = mirrors.first() | ||
| return redirect('repos:mirror_edit', mirror_id=mirror.id) | ||
| @@ -584,7 +603,13 @@ | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| parsed_params = parse_qs(filter_params, keep_blank_values=True) | ||
| single_valued_params = {k: v[0] for k, v in parsed_params.items() if v} | ||
| query_string = urlencode(single_valued_params) | ||
| redirect_url = reverse('repos:mirror_list') | ||
| if query_string: | ||
| redirect_url = f"{redirect_url}?{query_string}" | ||
| return redirect(redirect_url) | ||
| return redirect('repos:mirror_list') | ||
|
|
||
|
|
| if not selected_ids: | ||
| messages.warning(request, 'No mirrors selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
In general, the problem should be fixed by ensuring that any user-supplied value incorporated into a redirect URL is either (a) validated/whitelisted, or (b) safely sanitized so it cannot transform the redirect into an external or otherwise dangerous URL. For Django, protecting the path with reverse() is good, but we should also ensure that any appended query string is safe and only contains expected keys and values.
The best targeted fix here is to stop interpolating the raw filter_params string and instead rebuild a safe query string from parsed parameters. We already have a helper _get_filtered_mirrors(filter_params) that uses urllib.parse.parse_qs to parse filter_params into a dict and then only uses known keys: 'checksum', 'repo_id', and 'search'. We can use the same parsing approach to reconstruct a safe query dictionary limited to those known keys and then encode it with urllib.parse.urlencode. This removes any dangerous characters, strips unexpected parameters, and guarantees we only redirect with a controlled query string.
Concretely, inside mirror_bulk_action we should:
- Import
parse_qsandurlencodefromurllib.parseat the top of the file (or reuse the existing local import approach). - Add a small helper function (within the shown snippet)
_sanitize_filter_params(filter_params)that:- Parses the string with
parse_qs. - Builds a dict containing only known keys (
checksum,repo_id,search), taking the first value for each if present. - Returns
urlencode(safe_params)if there are any, else''.
- Parses the string with
- Replace all three uses of
f"{reverse('repos:mirror_list')}?{filter_params}"with code that calls this helper and only appends the?if the sanitized query string is non-empty.
This keeps the existing behavior (preserving filters as query params) while ensuring that only an allowed subset of parameters is included and that they are properly URL-encoded, eliminating the untrusted direct concatenation.
-
Copy modified line R27 -
Copy modified lines R44-R66 -
Copy modified lines R566-R568 -
Copy modified lines R578-R580 -
Copy modified lines R614-R616
| @@ -24,6 +24,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import parse_qs, urlencode | ||
|
|
||
| from arch.models import MachineArchitecture | ||
| from hosts.models import HostRepo | ||
| @@ -40,6 +41,29 @@ | ||
| from util.filterspecs import Filter, FilterBar | ||
|
|
||
|
|
||
| def _sanitize_filter_params(filter_params): | ||
| """ | ||
| Sanitize user-supplied filter parameters used for redirects. | ||
|
|
||
| Only allow known keys and return a safely URL-encoded query string. | ||
| """ | ||
| if not filter_params: | ||
| return '' | ||
|
|
||
| params = parse_qs(filter_params, keep_blank_values=True) | ||
| allowed_keys = {'checksum', 'repo_id', 'search'} | ||
| safe_params = {} | ||
| for key in allowed_keys: | ||
| if key in params and params[key]: | ||
| # take the first value for consistency with _get_filtered_mirrors | ||
| safe_params[key] = params[key][0] | ||
|
|
||
| if not safe_params: | ||
| return '' | ||
|
|
||
| return urlencode(safe_params) | ||
|
|
||
|
|
||
| @login_required | ||
| def repo_list(request): | ||
|
|
||
| @@ -507,7 +531,6 @@ | ||
|
|
||
| def _get_filtered_mirrors(filter_params): | ||
| """Helper to reconstruct filtered queryset from filter params.""" | ||
| from urllib.parse import parse_qs | ||
| params = parse_qs(filter_params) | ||
|
|
||
| mirrors = Mirror.objects.select_related().order_by('packages_checksum') | ||
| @@ -540,7 +563,9 @@ | ||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| safe_query = _sanitize_filter_params(filter_params) | ||
| if safe_query: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{safe_query}") | ||
| return redirect('repos:mirror_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -550,7 +575,9 @@ | ||
| if not selected_ids: | ||
| messages.warning(request, 'No mirrors selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| safe_query = _sanitize_filter_params(filter_params) | ||
| if safe_query: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{safe_query}") | ||
| return redirect('repos:mirror_list') | ||
| mirrors = Mirror.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -584,7 +611,9 @@ | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| safe_query = _sanitize_filter_params(filter_params) | ||
| if safe_query: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{safe_query}") | ||
| return redirect('repos:mirror_list') | ||
|
|
||
|
|
f326bae to
e4e4515
Compare
| if count != 1: | ||
| messages.warning(request, 'Please select exactly one mirror to edit') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
General approach: Avoid building redirect URLs via string concatenation with untrusted text. Instead, either (a) validate/whitelist the untrusted portion, or (b) pass structured data (a dict of query params) to redirect/reverse, letting Django construct the URL safely.
Best fix here: filter_params is expected to be an encoded querystring that _get_filtered_mirrors already parses using parse_qs. We can reuse parse_qs in mirror_bulk_action wherever we currently concatenate ?{filter_params} and instead do:
- Parse
filter_paramsinto a dict. - Flatten to a simple
dict[str, str](taking first value from each list). - Use
redirect('repos:mirror_list')when no filters, andredirect('repos:mirror_list') + "?" + urlencode(parsed)or better,redirectwith a URL built fromreverseandurlencode.
However, to stay within Django conventions and keep code simple and safe, we will:
- Import
parse_qsandurlencodeat the top (module level) instead of re‑importingparse_qsinside_get_filtered_mirrors. - Add a small helper
_build_mirror_list_redirect(filter_params: str)that:- If
filter_paramsis falsy, returnsredirect('repos:mirror_list'). - Otherwise parses it with
parse_qs, flattens, usesreverse('repos:mirror_list'), and appends a query string built withurlencode.
- If
- Replace all occurrences of
redirect(f"{reverse('repos:mirror_list')}?{filter_params}")and similar patterns withreturn _build_mirror_list_redirect(filter_params).
This preserves existing functionality (user stays on the mirror list with the same filters) while ensuring the redirect URL is constructed from structured, parsed values rather than raw text from the request.
Concretely, edits in repos/views.py:
- Add imports at the top:
from urllib.parse import parse_qs, urlencode. - Remove the inner
from urllib.parse import parse_qsin_get_filtered_mirrorsand use the top‑level import. - Define
_build_mirror_list_redirectabovemirror_bulk_action. - In
mirror_bulk_action, update all branches that currently doif filter_params: return redirect(f"{reverse('repos:mirror_list')}?{filter_params}")(lines 543–544, 553–554, 564–565, 586–588) to instead call the helper.
-
Copy modified line R27 -
Copy modified lines R530-R548 -
Copy modified line R561 -
Copy modified line R569 -
Copy modified line R578 -
Copy modified line R599
| @@ -24,6 +24,7 @@ | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| from rest_framework import viewsets | ||
| from urllib.parse import parse_qs, urlencode | ||
|
|
||
| from arch.models import MachineArchitecture | ||
| from hosts.models import HostRepo | ||
| @@ -507,7 +508,6 @@ | ||
|
|
||
| def _get_filtered_mirrors(filter_params): | ||
| """Helper to reconstruct filtered queryset from filter params.""" | ||
| from urllib.parse import parse_qs | ||
| params = parse_qs(filter_params) | ||
|
|
||
| mirrors = Mirror.objects.select_related().order_by('packages_checksum') | ||
| @@ -527,6 +527,25 @@ | ||
| return mirrors.distinct() | ||
|
|
||
|
|
||
| def _build_mirror_list_redirect(filter_params): | ||
| """ | ||
| Build a redirect response to the mirror list, preserving filter parameters. | ||
|
|
||
| This avoids concatenating untrusted query strings directly into the URL. | ||
| """ | ||
| if not filter_params: | ||
| return redirect('repos:mirror_list') | ||
|
|
||
| params = parse_qs(filter_params) | ||
| # Flatten parse_qs output: keep only the first value for each key. | ||
| flat_params = {key: values[0] for key, values in params.items() if values} | ||
| base_url = reverse('repos:mirror_list') | ||
| if not flat_params: | ||
| return redirect(base_url) | ||
| query_string = urlencode(flat_params, doseq=True) | ||
| return redirect(f"{base_url}?{query_string}") | ||
|
|
||
|
|
||
| @login_required | ||
| def mirror_bulk_action(request): | ||
| """Handle bulk actions on mirrors.""" | ||
| @@ -539,9 +558,7 @@ | ||
|
|
||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect('repos:mirror_list') | ||
| return _build_mirror_list_redirect(filter_params) | ||
|
|
||
| if select_all_filtered: | ||
| mirrors = _get_filtered_mirrors(filter_params) | ||
| @@ -549,9 +566,7 @@ | ||
| selected_ids = request.POST.getlist('selected_ids') | ||
| if not selected_ids: | ||
| messages.warning(request, 'No mirrors selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect('repos:mirror_list') | ||
| return _build_mirror_list_redirect(filter_params) | ||
| mirrors = Mirror.objects.filter(id__in=selected_ids) | ||
|
|
||
| count = mirrors.count() | ||
| @@ -560,9 +575,7 @@ | ||
| if action == 'edit': | ||
| if count != 1: | ||
| messages.warning(request, 'Please select exactly one mirror to edit') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect('repos:mirror_list') | ||
| return _build_mirror_list_redirect(filter_params) | ||
| mirror = mirrors.first() | ||
| return redirect('repos:mirror_edit', mirror_id=mirror.id) | ||
| elif action == 'enable': | ||
| @@ -583,9 +596,7 @@ | ||
| else: | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect('repos:mirror_list') | ||
| return _build_mirror_list_redirect(filter_params) | ||
|
|
||
|
|
||
| class RepositoryViewSet(viewsets.ModelViewSet): |
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
user-provided value
Untrusted URL redirection depends on a
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
In general, to fix untrusted URL redirection in Django, you must ensure that user-controlled data does not determine the redirect destination without validation. When you want to preserve filter/query parameters, you should either: (a) validate or sanitize those parameters into a safe form and then reconstruct the URL with Django’s URL utilities, or (b) avoid interpolating raw user strings into the redirect target and instead re-derive the filter parameters on the server side.
For this specific case, the redirect destination should always be the internal mirror_list view; filter_params is only meant to preserve filtering state as a query string. We can safely achieve this by using Django’s QueryDict to parse and rebuild filter_params into a properly encoded, relative query string. QueryDict.urlencode() only produces key=value pairs joined by &, so it cannot produce an absolute URL or inject a scheme/host into the redirect target. The fix is therefore:
- Import
QueryDictfromdjango.http(extending the existing import). - In
mirror_bulk_action, instead of directly usingf"{reverse('repos:mirror_list')}?{filter_params}", parsefilter_paramswithQueryDict, re-encode it with.urlencode(), and append that to the reversed path only if the result is non-empty. - Apply this pattern to each place in
mirror_bulk_actionwherefilter_paramsis used to construct the redirect.
Concretely:
- Modify the import at the top of
repos/views.pyfromfrom django.http import HttpResponsetofrom django.http import HttpResponse, QueryDict. - Inside
mirror_bulk_action, right after obtainingfilter_params, build a safe query string, e.g.:
raw_filter_params = request.POST.get('filter_params', '')
safe_query = QueryDict(raw_filter_params, mutable=False).urlencode()
if safe_query:
mirror_list_with_filters = f"{reverse('repos:mirror_list')}?{safe_query}"
else:
mirror_list_with_filters = reverse('repos:mirror_list')- Replace all occurrences of
f"{reverse('repos:mirror_list')}?{filter_params}"withmirror_list_with_filters. - Keep the existing behavior (redirecting back to the same list view with filters preserved) while ensuring the redirect URL is always relative and normalized.
-
Copy modified line R22 -
Copy modified lines R540-R549 -
Copy modified line R553 -
Copy modified line R560 -
Copy modified line R571 -
Copy modified line R594
| @@ -19,7 +19,7 @@ | ||
| from django.contrib.auth.decorators import login_required | ||
| from django.db import IntegrityError | ||
| from django.db.models import Count, Q | ||
| from django.http import HttpResponse | ||
| from django.http import HttpResponse, QueryDict | ||
| from django.shortcuts import get_object_or_404, redirect, render | ||
| from django.urls import reverse | ||
| from django_tables2 import RequestConfig | ||
| @@ -537,10 +537,20 @@ | ||
| select_all_filtered = request.POST.get('select_all_filtered') == '1' | ||
| filter_params = request.POST.get('filter_params', '') | ||
|
|
||
| # Sanitize filter_params by parsing and re-encoding as a query string. | ||
| # This ensures the redirect target remains a relative URL. | ||
| if filter_params: | ||
| safe_query = QueryDict(filter_params, mutable=False).urlencode() | ||
| mirror_list_with_filters = ( | ||
| f"{reverse('repos:mirror_list')}?{safe_query}" if safe_query else reverse('repos:mirror_list') | ||
| ) | ||
| else: | ||
| mirror_list_with_filters = reverse('repos:mirror_list') | ||
|
|
||
| if not action: | ||
| messages.warning(request, 'Please select an action') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect(mirror_list_with_filters) | ||
| return redirect('repos:mirror_list') | ||
|
|
||
| if select_all_filtered: | ||
| @@ -550,7 +557,7 @@ | ||
| if not selected_ids: | ||
| messages.warning(request, 'No mirrors selected') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect(mirror_list_with_filters) | ||
| return redirect('repos:mirror_list') | ||
| mirrors = Mirror.objects.filter(id__in=selected_ids) | ||
|
|
||
| @@ -561,7 +568,7 @@ | ||
| if count != 1: | ||
| messages.warning(request, 'Please select exactly one mirror to edit') | ||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect(mirror_list_with_filters) | ||
| return redirect('repos:mirror_list') | ||
| mirror = mirrors.first() | ||
| return redirect('repos:mirror_edit', mirror_id=mirror.id) | ||
| @@ -584,7 +591,7 @@ | ||
| messages.warning(request, 'Invalid action') | ||
|
|
||
| if filter_params: | ||
| return redirect(f"{reverse('repos:mirror_list')}?{filter_params}") | ||
| return redirect(mirror_list_with_filters) | ||
| return redirect('repos:mirror_list') | ||
|
|
||
|
|
No description provided.