GHSA-W2J7-F3C6-G8CW
Vulnerability from github – Published: 2026-06-23 22:46 – Updated: 2026-06-23 22:46Open Redirect in Flask-Security
Summary
flask_security.utils.validate_redirect_url() can allow an attacker-controlled redirect URL when subdomain redirects are enabled.
The bypass uses a backslash inside the URL authority/host:
http://evil.com\.whitelist.com
http://evil.com%5C.whitelist.com
Python's urlsplit() parses the full authority as evil.com\.whitelist.com or evil.com%5C.whitelist.com. Because the value ends with .whitelist.com, validate_redirect_url() accepts it as an allowed subdomain of whitelist.com.
This is similar in class to the previous Flask-Security-Too open redirect advisory CVE-2023-49438 / GHSA-672h-6x89-76m5, where crafted redirect URLs bypassed validation through browser URL normalization behavior.
Affected Configuration
The issue requires subdomain redirects to be enabled:
SERVER_NAME = "whitelist.com"
SECURITY_REDIRECT_ALLOW_SUBDOMAINS = True
Tested environment:
Flask-Security: 5.8.0
Flask: 3.1.3
Werkzeug: 3.1.8
Impact
An attacker can craft a URL that passes Flask-Security's redirect validation and produces a 302 response to an attacker-controlled URL-like authority.
This can be used for phishing or other attacks that rely on a trusted application redirecting users to an attacker-controlled destination.
Proof of Concept
PoC Flask App
from __future__ import annotations
from importlib.metadata import version
from urllib.parse import urlsplit
from flask import Flask, jsonify, redirect, request
from flask_security.utils import validate_redirect_url
app = Flask(__name__)
app.config.update(
SECRET_KEY="poc-only",
SERVER_NAME="whitelist.com",
SECURITY_REDIRECT_ALLOW_SUBDOMAINS=True,
SECURITY_REDIRECT_BASE_DOMAIN=None,
SECURITY_REDIRECT_ALLOWED_SUBDOMAINS=[],
)
@app.get("/")
def index():
return jsonify(
flask_version=version("Flask"),
configured_server_name=app.config["SERVER_NAME"],
examples=[
r"http://evil.com\.whitelist.com",
"http://evil.com%5C.whitelist.com",
"http://sub.whitelist.com",
"http://sub.not-whitelist.com",
],
)
@app.get("/check")
def check():
next_url = request.args.get("next", "")
parsed = urlsplit(next_url)
return jsonify(
next=next_url,
valid=validate_redirect_url(next_url),
parsed={
"scheme": parsed.scheme,
"netloc": parsed.netloc,
"hostname": parsed.hostname,
"path": parsed.path,
},
)
@app.get("/redir")
def redir():
next_url = request.args.get("next", "")
if not validate_redirect_url(next_url):
return jsonify(error="blocked", next=next_url), 400
return redirect(next_url)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
Steps to Reproduce
Run the PoC with the target project's Flask version:
.venv/bin/python poc_redirect_app.py
The invalid comparison case is correctly blocked:
http://127.0.0.1:5000/redir?next=http://evil.com
Observed result:
Check the validation result:
http://127.0.0.1:5000/check?next=http://evil.com%5C.whitelist.com
Observed result:
References
- CVE-2023-49438: https://advisories.gitlab.com/pypi/flask-security-too/CVE-2023-49438/
- GHSA-672h-6x89-76m5: https://osv.dev/vulnerability/CVE-2023-49438
- NVD entry for CVE-2023-49438: https://nvd.nist.gov/vuln/detail/CVE-2023-49438
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.8.0"
},
"package": {
"ecosystem": "PyPI",
"name": "Flask-Security"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.8.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-601"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-23T22:46:31Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "# Open Redirect in Flask-Security\n\n## Summary\n\n`flask_security.utils.validate_redirect_url()` can allow an attacker-controlled redirect URL when subdomain redirects are enabled.\n\nThe bypass uses a backslash inside the URL authority/host:\n\n```text\nhttp://evil.com\\.whitelist.com\nhttp://evil.com%5C.whitelist.com\n```\n\nPython\u0027s `urlsplit()` parses the full authority as `evil.com\\.whitelist.com` or `evil.com%5C.whitelist.com`. Because the value ends with `.whitelist.com`, `validate_redirect_url()` accepts it as an allowed subdomain of `whitelist.com`.\n\nThis is similar in class to the previous Flask-Security-Too open redirect advisory CVE-2023-49438 / GHSA-672h-6x89-76m5, where crafted redirect URLs bypassed validation through browser URL normalization behavior.\n\n## Affected Configuration\n\nThe issue requires subdomain redirects to be enabled:\n\n```python\nSERVER_NAME = \"whitelist.com\"\nSECURITY_REDIRECT_ALLOW_SUBDOMAINS = True\n```\n\nTested environment:\n\n```text\nFlask-Security: 5.8.0\nFlask: 3.1.3\nWerkzeug: 3.1.8\n```\n\n## Impact\n\nAn attacker can craft a URL that passes Flask-Security\u0027s redirect validation and produces a `302` response to an attacker-controlled URL-like authority.\n\nThis can be used for phishing or other attacks that rely on a trusted application redirecting users to an attacker-controlled destination.\n\n## Proof of Concept\n\n### PoC Flask App\n\n```python\nfrom __future__ import annotations\n\nfrom importlib.metadata import version\nfrom urllib.parse import urlsplit\n\nfrom flask import Flask, jsonify, redirect, request\n\nfrom flask_security.utils import validate_redirect_url\n\n\napp = Flask(__name__)\napp.config.update(\n SECRET_KEY=\"poc-only\",\n SERVER_NAME=\"whitelist.com\",\n SECURITY_REDIRECT_ALLOW_SUBDOMAINS=True,\n SECURITY_REDIRECT_BASE_DOMAIN=None,\n SECURITY_REDIRECT_ALLOWED_SUBDOMAINS=[],\n)\n\n\n@app.get(\"/\")\ndef index():\n return jsonify(\n flask_version=version(\"Flask\"),\n configured_server_name=app.config[\"SERVER_NAME\"],\n examples=[\n r\"http://evil.com\\.whitelist.com\",\n \"http://evil.com%5C.whitelist.com\",\n \"http://sub.whitelist.com\",\n \"http://sub.not-whitelist.com\",\n ],\n )\n\n\n@app.get(\"/check\")\ndef check():\n next_url = request.args.get(\"next\", \"\")\n parsed = urlsplit(next_url)\n\n return jsonify(\n next=next_url,\n valid=validate_redirect_url(next_url),\n parsed={\n \"scheme\": parsed.scheme,\n \"netloc\": parsed.netloc,\n \"hostname\": parsed.hostname,\n \"path\": parsed.path,\n },\n )\n\n\n@app.get(\"/redir\")\ndef redir():\n next_url = request.args.get(\"next\", \"\")\n if not validate_redirect_url(next_url):\n return jsonify(error=\"blocked\", next=next_url), 400\n\n return redirect(next_url)\n\n\nif __name__ == \"__main__\":\n app.run(host=\"127.0.0.1\", port=5000, debug=False)\n```\n\n### Steps to Reproduce\n\nRun the PoC with the target project\u0027s Flask version:\n\n```bash\n.venv/bin/python poc_redirect_app.py\n```\n\nThe invalid comparison case is correctly blocked:\n\n```bash\nhttp://127.0.0.1:5000/redir?next=http://evil.com\n```\n\nObserved result:\n\n\u003cimg width=\"1019\" height=\"294\" alt=\"image\" src=\"https://github.com/user-attachments/assets/de25ac4d-b37f-4369-928e-f44dfd5b7557\" /\u003e\n\nCheck the validation result:\n\n```bash\nhttp://127.0.0.1:5000/check?next=http://evil.com%5C.whitelist.com\n```\n\nObserved result:\n\n\u003cimg width=\"1029\" height=\"634\" alt=\"image\" src=\"https://github.com/user-attachments/assets/8e5ec8a6-42a4-438a-8d12-a27724519091\" /\u003e\n\n## References\n\n- CVE-2023-49438: https://advisories.gitlab.com/pypi/flask-security-too/CVE-2023-49438/\n- GHSA-672h-6x89-76m5: https://osv.dev/vulnerability/CVE-2023-49438\n- NVD entry for CVE-2023-49438: https://nvd.nist.gov/vuln/detail/CVE-2023-49438",
"id": "GHSA-w2j7-f3c6-g8cw",
"modified": "2026-06-23T22:46:31Z",
"published": "2026-06-23T22:46:31Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pallets-eco/flask-security/security/advisories/GHSA-w2j7-f3c6-g8cw"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-49438"
},
{
"type": "WEB",
"url": "https://advisories.gitlab.com/pypi/flask-security-too/CVE-2023-49438"
},
{
"type": "PACKAGE",
"url": "https://github.com/pallets-eco/flask-security"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2023-49438"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Flask-Security has an Open Redirect issue"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.