# PYTHON-FLASK-SEC-018: Flask Hashids with Secret Key as Salt

> **Severity:** MEDIUM | **CWE:** CWE-327 | **OWASP:** A02:2021

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-018
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-018 --project .`

## Description

This rule detects Flask applications that initialize Hashids with Flask's secret key as the
salt: Hashids(salt=app.secret_key). Hashids is a library for encoding integers into
short URL-friendly strings. It is not a cryptographic hash function -- it is a
bidirectional encoding scheme, and the salt is recoverable.

In 2015, security researcher Phil Carnage published a cryptanalysis showing that the Hashids
salt can be recovered with approximately 20 known plaintext pairs (input integer and
corresponding hashid). This means an attacker who observes enough hashids in your application's
URLs can reverse-engineer the salt value. If the salt is Flask's secret_key, the attacker
now has the key used to sign session cookies, CSRF tokens, and any other Flask security
primitive that depends on SECRET_KEY. With the secret key, the attacker can forge arbitrary
session cookies and authenticate as any user, including administrators.

The detection uses HashidsModule.method("Hashids").where("salt", "app.secret_key") -- a
QueryType-based precise match. HashidsModule declares fqns=["hashids"], so only Hashids
objects imported from the hashids package are matched. The .where("salt", "app.secret_key")
filter means only calls where the salt keyword argument is specifically the Flask secret key
attribute are flagged. Using a separate, dedicated salt value for Hashids does not trigger
this rule. This precision delivers zero false positives on correctly configured applications.


## Vulnerable Code

```python
from flask import Flask
from hashids import Hashids

app = Flask(__name__)
app.secret_key = 'my-secret'
hasher = Hashids(salt=app.secret_key)
```

## Secure Code

```python
import os
from flask import Flask
from hashids import Hashids

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)

# SAFE: Use a separate, independently generated salt for Hashids.
# This salt can be compromised without exposing the Flask secret key.
HASHIDS_SALT = os.getenv('HASHIDS_SALT', os.urandom(16).hex())
hashids = Hashids(salt=HASHIDS_SALT, min_length=8)

# NEVER do this:
# hashids = Hashids(salt=app.secret_key)  # Exposes SECRET_KEY via cryptanalysis

@app.route('/user/<int:user_id>')
def user_profile(user_id):
    encoded = hashids.encode(user_id)
    return {'id': encoded}

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, Or, QueryType

class HashidsModule(QueryType):
    fqns = ["hashids"]


@python_rule(
    id="PYTHON-FLASK-SEC-018",
    name="Flask Hashids with Secret Key",
    severity="MEDIUM",
    category="flask",
    cwe="CWE-330",
    tags="python,flask,hashids,secret-key,CWE-330",
    message="Flask SECRET_KEY used as Hashids salt. Use a separate salt value.",
    owasp="A02:2021",
)
def detect_flask_hashids_secret():
    """Detects Hashids(salt=app.secret_key)."""
    return HashidsModule.method("Hashids").where("salt", "app.secret_key")
```

## How to Fix

- Use a separate, independently generated value for the Hashids salt -- for example, an environment variable HASHIDS_SALT that is distinct from FLASK_SECRET_KEY.
- Rotate the Hashids salt independently of the Flask secret key so a compromise of one does not affect the other.
- Understand that Hashids is not a security primitive -- it is an obfuscation library. Do not rely on Hashids to protect access control. Always enforce authorization server-side regardless of whether the ID is encoded.
- If you need cryptographically secure token generation for URLs, use itsdangerous.URLSafeTimedSerializer with a dedicated signing key rather than Hashids.
- Store sensitive key material (secret keys, salts) in a secrets manager or environment variables, never hardcoded in source code.

## Security Implications

- **Flask Secret Key Exposure via Hashids Cryptanalysis:** With approximately 20 known (integer, hashid) pairs -- easily obtained from any
paginated list, user profile URL, or content ID that exposes hashids -- an attacker
can recover the Hashids salt through known-plaintext cryptanalysis. If that salt is
app.secret_key, the attacker has the key used to sign everything Flask protects with
it.

- **Session Cookie Forgery:** Flask's session cookie is signed with SECRET_KEY using itsdangerous. Once an attacker
obtains the secret key, they can forge a session cookie with any user ID -- including
admin accounts -- without ever knowing a password or exploiting authentication logic.

- **CSRF Token Bypass:** Flask-WTF's CSRF tokens are also derived from SECRET_KEY. With the key recovered,
an attacker can generate valid CSRF tokens for any target user's session, bypassing
CSRF protection on all state-changing endpoints.

- **Itsdangerous Token Forgery:** Any itsdangerous-based token (password reset links, email confirmation tokens, timed
tokens) that the application generates using SECRET_KEY can be forged by an attacker
who possesses the key.


## FAQ

**Q: How can the Hashids salt be recovered? Isn't it hidden inside the encoding?**

Hashids uses the salt in a way that is mathematically reversible given enough known
plaintext pairs. The 2015 cryptanalysis by Phil Carnage demonstrated recovery with
approximately 20 (input, output) pairs. In a web application that exposes encoded IDs
in URLs or API responses, an attacker can easily collect these pairs by incrementing
input integers and observing the outputs.


**Q: Why does this rule use .where() instead of a broader pattern?**

The .where("salt", "app.secret_key") constraint makes this a zero-false-positive rule.
It only flags the specific case where the salt argument is Flask's secret key attribute.
Hashids with any other salt value is not flagged, even if that other value might also
be sensitive. The precise match means every finding is a confirmed misuse of the secret
key, not a general Hashids audit.


**Q: Is Hashids a cryptographic library?**

No. Hashids is explicitly documented as an obfuscation library, not a cryptographic one.
The author warns against using it for security purposes. It encodes integers into
short strings in a way that is aesthetically pleasing and reversible. It provides no
security guarantees against an adversary who can make observations.


**Q: What should I use for secure URL tokens instead of Hashids?**

For URL-safe tokens that need to be tamper-evident, use itsdangerous.URLSafeTimedSerializer
or URLSafeSerializer with a dedicated signing key. For simple obfuscation without security
requirements, use a UUID or a different encoding scheme. For access-controlled resources,
enforce authorization server-side -- the token format does not substitute for access control.


**Q: How do I run this rule in CI/CD?**

Run: pathfinder ci --ruleset python/flask/PYTHON-FLASK-SEC-018 --project .
The rule outputs SARIF, JSON, or CSV and can post inline pull request comments on GitHub.


**Q: Does this rule catch app.config["SECRET_KEY"] used as the Hashids salt?**

No. This rule matches the specific attribute access pattern app.secret_key. Using
app.config["SECRET_KEY"] as the salt is an equivalent vulnerability but a different
code pattern. Review your codebase for both forms when addressing this finding.


## References

- [CWE-327: Use of a Broken or Risky Cryptographic Algorithm](https://cwe.mitre.org/data/definitions/327.html)
- [Hashids Cryptanalysis by Phil Carnage (2015)](http://carnage.github.io/2015/08/cryptanalysis-of-hashids)
- [Flask SECRET_KEY Documentation](https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY)
- [itsdangerous Documentation](https://itsdangerous.palletsprojects.com/)
- [OWASP A02:2021 Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
- [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)

---

Source: https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-018
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
