GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-MP82-FMJ6-F22V

Vulnerability from github – Published: 2026-04-16 01:20 – Updated: 2026-06-08 19:47
VLAI
Summary
pyLoad has a Session Cookie Security Downgrade via Untrusted X-Forwarded-Proto Header Spoofing (Global State Race Condition)
Details

Summary

The set_session_cookie_secure before_request handler in src/pyload/webui/app/__init__.py reads the X-Forwarded-Proto header from any HTTP request without validating that the request originates from a trusted proxy, then mutates the global Flask configuration SESSION_COOKIE_SECURE on every request. Because pyLoad uses the multi-threaded Cheroot WSGI server (request_queue_size=512), this creates a race condition where an attacker's request can influence the Secure flag on other users' session cookies — either downgrading cookie security behind a TLS proxy or causing a session denial-of-service on plain HTTP deployments.

Details

The vulnerable code is in src/pyload/webui/app/__init__.py:75-84:

# Dynamically set SESSION_COOKIE_SECURE according to the value of X-Forwarded-Proto
# TODO: Add trusted proxy check
@app.before_request
def set_session_cookie_secure():
    x_forwarded_proto = flask.request.headers.get("X-Forwarded-Proto", "")
    is_secure = (
        x_forwarded_proto.split(',')[0].strip() == "https" or
        app.config["PYLOAD_API"].get_config_value("webui", "use_ssl")
    )
    flask.current_app.config['SESSION_COOKIE_SECURE'] = is_secure

The root cause has two components:

  1. No origin validation (CWE-346): The X-Forwarded-Proto header is read from any client request. This header is only trustworthy when set by a known reverse proxy. Without ProxyFix middleware or a trusted proxy allowlist, any client can spoof it. The code itself acknowledges this with the TODO on line 76.

  2. Global state mutation in a multi-threaded server: flask.current_app.config['SESSION_COOKIE_SECURE'] is application-wide shared state. When Thread A (attacker) writes False to this config, Thread B (victim) may read False when Flask's save_session() runs in the after_request phase, producing a Set-Cookie response without the Secure flag.

The Cheroot WSGI server is configured with request_queue_size=512 in src/pyload/webui/webserver_thread.py:46, confirming concurrent multi-threaded request processing.

No ProxyFix or equivalent middleware is configured anywhere in the codebase (confirmed via codebase-wide search).

PoC

Attack Path 1 — Cookie Security Downgrade (behind TLS-terminating proxy, use_ssl=False):

An attacker with direct access to the backend (e.g., in a containerized/Kubernetes deployment) sends concurrent requests to keep SESSION_COOKIE_SECURE set to False:

# Attacker floods backend directly, bypassing TLS proxy
for i in $(seq 1 200); do
  curl -s -H 'X-Forwarded-Proto: http' http://pyload-backend:8000/ &
done

# Meanwhile, a legitimate user behind the TLS proxy receives a session cookie
# During the race window, their Set-Cookie header lacks the Secure flag
# The cookie is then vulnerable to interception over plain HTTP

Attack Path 2 — Session Denial of Service (default plain HTTP deployment):

# Attacker causes SESSION_COOKIE_SECURE=True on a plain HTTP server
for i in $(seq 1 200); do
  curl -s -H 'X-Forwarded-Proto: https' http://localhost:8000/ &
done

# Concurrent legitimate users receive Set-Cookie with Secure flag
# Browser refuses to send Secure cookies over HTTP
# Users' sessions silently break — they appear logged out

The second attack path works against the default configuration (use_ssl=False) and requires no special network position.

Impact

  • Session cookie exposure (Attack Path 1): When deployed behind a TLS-terminating proxy, an attacker can cause session cookies to be issued without the Secure flag. If the victim's browser subsequently makes an HTTP request (e.g., via a mixed-content link or downgrade attack), the session cookie is transmitted in cleartext, enabling session hijacking.

  • Session denial of service (Attack Path 2): On default plain HTTP deployments, an attacker can continuously set SESSION_COOKIE_SECURE=True, causing browsers to refuse sending session cookies back to the server. This silently breaks all concurrent users' sessions with no user-visible error message, only a redirect to login.

  • No authentication required: Both attack paths are fully unauthenticated — the before_request handler fires before any auth checks.

Recommended Fix

Replace the global config mutation with per-response cookie handling, and add proxy validation:

# Option A: Set Secure flag per-response instead of mutating global config
@app.after_request
def set_session_cookie_secure(response):
    # Only trust X-Forwarded-Proto if ProxyFix is configured
    is_secure = app.config["PYLOAD_API"].get_config_value("webui", "use_ssl")
    if 'Set-Cookie' in response.headers:
        # Modify cookie flags per-response, not global config
        cookies = response.headers.getlist('Set-Cookie')
        response.headers.remove('Set-Cookie')
        for cookie in cookies:
            if is_secure and 'Secure' not in cookie:
                cookie += '; Secure'
            response.headers.add('Set-Cookie', cookie)
    return response

# Option B (preferred): Use Werkzeug's ProxyFix with explicit trust
from werkzeug.middleware.proxy_fix import ProxyFix

# In App.__new__, before returning:
if trusted_proxy_count:  # from config
    app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=trusted_proxy_count)
# Then set SESSION_COOKIE_SECURE once at startup based on use_ssl config,
# and let ProxyFix handle X-Forwarded-Proto transparently

At minimum, remove the before_request handler entirely and set SESSION_COOKIE_SECURE once at startup (line 130 already does this in _configure_session). The dynamic per-request adjustment is the root cause of both the spoofing and the race condition.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.5.0b3.dev97"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "pyload-ng"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.5.0b3.dev98"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40594"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-346"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T01:20:49Z",
    "nvd_published_at": "2026-04-21T18:16:51Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe `set_session_cookie_secure` `before_request` handler in `src/pyload/webui/app/__init__.py` reads the `X-Forwarded-Proto` header from any HTTP request without validating that the request originates from a trusted proxy, then mutates the **global** Flask configuration `SESSION_COOKIE_SECURE` on every request. Because pyLoad uses the multi-threaded Cheroot WSGI server (`request_queue_size=512`), this creates a race condition where an attacker\u0027s request can influence the `Secure` flag on other users\u0027 session cookies \u2014 either downgrading cookie security behind a TLS proxy or causing a session denial-of-service on plain HTTP deployments.\n\n## Details\n\nThe vulnerable code is in `src/pyload/webui/app/__init__.py:75-84`:\n\n```python\n# Dynamically set SESSION_COOKIE_SECURE according to the value of X-Forwarded-Proto\n# TODO: Add trusted proxy check\n@app.before_request\ndef set_session_cookie_secure():\n    x_forwarded_proto = flask.request.headers.get(\"X-Forwarded-Proto\", \"\")\n    is_secure = (\n        x_forwarded_proto.split(\u0027,\u0027)[0].strip() == \"https\" or\n        app.config[\"PYLOAD_API\"].get_config_value(\"webui\", \"use_ssl\")\n    )\n    flask.current_app.config[\u0027SESSION_COOKIE_SECURE\u0027] = is_secure\n```\n\nThe root cause has two components:\n\n1. **No origin validation (CWE-346):** The `X-Forwarded-Proto` header is read from any client request. This header is only trustworthy when set by a known reverse proxy. Without `ProxyFix` middleware or a trusted proxy allowlist, any client can spoof it. The code itself acknowledges this with the TODO on line 76.\n\n2. **Global state mutation in a multi-threaded server:** `flask.current_app.config[\u0027SESSION_COOKIE_SECURE\u0027]` is application-wide shared state. When Thread A (attacker) writes `False` to this config, Thread B (victim) may read `False` when Flask\u0027s `save_session()` runs in the after_request phase, producing a `Set-Cookie` response without the `Secure` flag.\n\nThe Cheroot WSGI server is configured with `request_queue_size=512` in `src/pyload/webui/webserver_thread.py:46`, confirming concurrent multi-threaded request processing.\n\nNo `ProxyFix` or equivalent middleware is configured anywhere in the codebase (confirmed via codebase-wide search).\n\n## PoC\n\n**Attack Path 1 \u2014 Cookie Security Downgrade (behind TLS-terminating proxy, `use_ssl=False`):**\n\nAn attacker with direct access to the backend (e.g., in a containerized/Kubernetes deployment) sends concurrent requests to keep `SESSION_COOKIE_SECURE` set to `False`:\n\n```bash\n# Attacker floods backend directly, bypassing TLS proxy\nfor i in $(seq 1 200); do\n  curl -s -H \u0027X-Forwarded-Proto: http\u0027 http://pyload-backend:8000/ \u0026\ndone\n\n# Meanwhile, a legitimate user behind the TLS proxy receives a session cookie\n# During the race window, their Set-Cookie header lacks the Secure flag\n# The cookie is then vulnerable to interception over plain HTTP\n```\n\n**Attack Path 2 \u2014 Session Denial of Service (default plain HTTP deployment):**\n\n```bash\n# Attacker causes SESSION_COOKIE_SECURE=True on a plain HTTP server\nfor i in $(seq 1 200); do\n  curl -s -H \u0027X-Forwarded-Proto: https\u0027 http://localhost:8000/ \u0026\ndone\n\n# Concurrent legitimate users receive Set-Cookie with Secure flag\n# Browser refuses to send Secure cookies over HTTP\n# Users\u0027 sessions silently break \u2014 they appear logged out\n```\n\nThe second attack path works against the default configuration (`use_ssl=False`) and requires no special network position.\n\n## Impact\n\n- **Session cookie exposure (Attack Path 1):** When deployed behind a TLS-terminating proxy, an attacker can cause session cookies to be issued without the `Secure` flag. If the victim\u0027s browser subsequently makes an HTTP request (e.g., via a mixed-content link or downgrade attack), the session cookie is transmitted in cleartext, enabling session hijacking.\n\n- **Session denial of service (Attack Path 2):** On default plain HTTP deployments, an attacker can continuously set `SESSION_COOKIE_SECURE=True`, causing browsers to refuse sending session cookies back to the server. This silently breaks all concurrent users\u0027 sessions with no user-visible error message, only a redirect to login.\n\n- **No authentication required:** Both attack paths are fully unauthenticated \u2014 the `before_request` handler fires before any auth checks.\n\n## Recommended Fix\n\nReplace the global config mutation with per-response cookie handling, and add proxy validation:\n\n```python\n# Option A: Set Secure flag per-response instead of mutating global config\n@app.after_request\ndef set_session_cookie_secure(response):\n    # Only trust X-Forwarded-Proto if ProxyFix is configured\n    is_secure = app.config[\"PYLOAD_API\"].get_config_value(\"webui\", \"use_ssl\")\n    if \u0027Set-Cookie\u0027 in response.headers:\n        # Modify cookie flags per-response, not global config\n        cookies = response.headers.getlist(\u0027Set-Cookie\u0027)\n        response.headers.remove(\u0027Set-Cookie\u0027)\n        for cookie in cookies:\n            if is_secure and \u0027Secure\u0027 not in cookie:\n                cookie += \u0027; Secure\u0027\n            response.headers.add(\u0027Set-Cookie\u0027, cookie)\n    return response\n\n# Option B (preferred): Use Werkzeug\u0027s ProxyFix with explicit trust\nfrom werkzeug.middleware.proxy_fix import ProxyFix\n\n# In App.__new__, before returning:\nif trusted_proxy_count:  # from config\n    app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=trusted_proxy_count)\n# Then set SESSION_COOKIE_SECURE once at startup based on use_ssl config,\n# and let ProxyFix handle X-Forwarded-Proto transparently\n```\n\nAt minimum, remove the `before_request` handler entirely and set `SESSION_COOKIE_SECURE` once at startup (line 130 already does this in `_configure_session`). The dynamic per-request adjustment is the root cause of both the spoofing and the race condition.",
  "id": "GHSA-mp82-fmj6-f22v",
  "modified": "2026-06-08T19:47:41Z",
  "published": "2026-04-16T01:20:49Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pyload/pyload/security/advisories/GHSA-mp82-fmj6-f22v"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40594"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pyload/pyload"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pypa/advisory-database/tree/main/vulns/pyload-ng/PYSEC-2026-125.yaml"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "pyLoad has a Session Cookie Security Downgrade via Untrusted X-Forwarded-Proto Header Spoofing (Global State Race Condition)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…