CVE-2024-24808 (published 2024-02-06T04:15Z)

pyLoad is an open-source Download Manager written in pure Python. There is an open redirect vulnerability due to incorrect validation of input values when redirecting users after login. pyLoad is validating URLs via the `get_redirect_url` function when redirecting users at login. This vulnerability has been patched with commit fe94451.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/pyload/pyload
commit_details: repo_url: https://github.com/pyload/pyload
author: GammaC0de
author_date: 2024-02-06 00:19:52+02:00
msg: fix GHSA-g3cm-qg2v-2hj5 security advisory
repository: repo_url: https://github.com/pyload/pyload
repo_name: pyload/pyload
description: The free and open-source Download Manager written in pure Python
repo_language: Python
owner: pyload
file_changes: filename: app_blueprint.py
change_type: ModificationType.MODIFY
diff: @@ -47,7 +47,7 @@ def robots():
 def login():
     api = flask.current_app.config["PYLOAD_API"]
 
-    next = get_redirect_url(fallback=flask.url_for("app.dashboard"))
+    next_url = get_redirect_url(fallback="app.dashboard")
 
     if flask.request.method == "POST":
         user = flask.request.form["username"]
@@ -57,14 +57,14 @@ def login():
         sanitized_user = user.replace("\n", "\\n").replace("\r", "\\r")
         if not user_info:
             log.error(f"Login failed for user '{sanitized_user}'")
-            return render_template("login.html", next=next, errors=True)
+            return render_template("login.html", errors=True)
 
         set_session(user_info)
         log.info(f"User '{sanitized_user}' successfully logged in")
         flask.flash("Logged in successfully")
 
     if is_authenticated():
-        return flask.redirect(next)
+        return flask.redirect(next_url)
 
     if api.get_config_value("webui", "autologin"):
         allusers = api.get_all_userdata()
@@ -74,9 +74,9 @@ def login():
             # NOTE: Double-check authentication here because if session[name] is empty,
             #       next login_required redirects here again and all loop out.
             if is_authenticated():
-                return flask.redirect(next)
+                return flask.redirect(next_url)
 
-    return render_template("login.html", next=next)
+    return render_template("login.html")
 
 
 @bp.route("/logout", endpoint="logout")

programming_language: Python
filename: helpers.py
change_type: ModificationType.MODIFY
diff: @@ -2,10 +2,11 @@
 
 import json
 from functools import wraps
-from urllib.parse import unquote, urljoin, urlparse
+from urllib.parse import urljoin, urlparse
 
 import flask
 import flask_themes2
+import werkzeug.routing.exceptions
 from pyload.core.api import Perms, Role, has_permission
 
 
@@ -33,22 +34,25 @@ else:
 
 #: Checks if location belongs to same host address
 def is_safe_url(location):
-    ref_url = urlparse(flask.request.host_url)
-    test_url = urlparse(urljoin(flask.request.host_url, location))
-    return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
+    location_urlp = urlparse(location)
+    #: if relative URL then must start with "/"
+    if not location_urlp.netloc and location[0] != "/":
+        return False
+    host_urlp = urlparse(flask.request.host_url)
+    test_urlp = urlparse(urljoin(flask.request.host_url, location))
+    return test_urlp.scheme in ('http', 'https') and host_urlp.netloc == test_urlp.netloc
 
 
 def get_redirect_url(fallback=None):
-    login_url = urljoin(flask.request.url_root, flask.url_for('app.login'))
-    request_url = unquote(flask.request.url)
-    for location in flask.request.values.get("next"), flask.request.referrer:
-        if not location:
-            continue
-        if location in (request_url, login_url):  # don't redirect to same location
-            continue
-        if is_safe_url(location):
-            return location
-    return fallback
+    next_arg = flask.request.values.get("next")
+    redirect_url = flask.url_for(fallback)
+    if next_arg and next_arg != "login":  # don't redirect to same location
+        try:
+            redirect_url = flask.url_for(f"app.{next_arg}")
+        except werkzeug.routing.exceptions.BuildError:
+            pass
+
+    return urljoin(flask.request.url_root, redirect_url)
 
 
 def render_base(messages):
@@ -206,7 +210,7 @@ def login_required(perm):
                 else:
                     location = flask.url_for(
                         "app.login",
-                        next=flask.request.url
+                        next=flask.request.endpoint.split(".")[-1]
                     )
                     response = flask.redirect(location)
 

programming_language: Python