Every CVESearch/Get API key

GHSA-w2j7-f3c6-g8cw

Flask-Security has an Open Redirect issue

4.7 MediumFix available

GHSA-w2j7-f3c6-g8cw is a medium vulnerability (CVSS 4.7) in flask-security, published June 23, 2026. No source lists public exploit code. Fixed in flask-security 5.8.1.

What does this mean?
Severity
4.7 MediumCVSS 3.1, GitHub Advisories
Exploit chance
UnscoredNo EPSS score yet
Exploitation
None knownNot on CISA’s or ENISA’s lists

Description

Open 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:

<img width="1019" height="294" alt="image" src="https://github.com/user-attachments/assets/de25ac4d-b37f-4369-928e-f44dfd5b7557" />

Check the validation result:

http://127.0.0.1:5000/check?next=http://evil.com%5C.whitelist.com

Observed result:

<img width="1029" height="634" alt="image" src="https://github.com/user-attachments/assets/8e5ec8a6-42a4-438a-8d12-a27724519091" />

References

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
Required

What an attacker can affect

Confidentiality
Low
Integrity
None
Availability
None

Affected packages

Affected packages
PackageAffectedFixed
flask-security
PyPI
through 5.8.0
5.8.1
flask-security
PyPI
before 5.8.1, plus listed versions
1.2.01.2.11.2.21.2.31.5.0
Show 29 moreShow less
1.5.11.5.21.5.31.5.41.6.01.6.11.6.21.6.31.6.41.6.51.6.61.6.71.6.81.6.91.7.01.7.11.7.21.7.31.7.41.7.53.0.05.5.15.5.25.6.05.6.15.6.25.7.05.7.15.8.0
5.8.1

What each source reports

GitHub Advisories Score used4.7

Flask-Security has an Open Redirect issue

Open 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:

<img width="1019" height="294" alt="image" src="https://github.com/user-attachments/assets/de25ac4d-b37f-4369-928e-f44dfd5b7557" />

Check the validation result:

http://127.0.0.1:5000/check?next=http://evil.com%5C.whitelist.com

Observed result:

<img width="1029" height="634" alt="image" src="https://github.com/user-attachments/assets/8e5ec8a6-42a4-438a-8d12-a27724519091" />

References

Record
GHSA-w2j7-f3c6-g8cw
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
Weakness
CWE-601
Updated
2026-11-02
OSV4.7

Flask-Security has an Open Redirect issue

Open 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:

<img width="1019" height="294" alt="image" src="https://github.com/user-attachments/assets/de25ac4d-b37f-4369-928e-f44dfd5b7557" />

Check the validation result:

http://127.0.0.1:5000/check?next=http://evil.com%5C.whitelist.com

Observed result:

<img width="1029" height="634" alt="image" src="https://github.com/user-attachments/assets/8e5ec8a6-42a4-438a-8d12-a27724519091" />

References

Record
GHSA-w2j7-f3c6-g8cw
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
Weakness
CWE-601
Updated
2026-11-02

History

History
RecordedChangedSources
first indexedGitHub Advisories, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

References

MoveEnter Open