fix: replace unsalted MD5 password hashing with Django PBKDF2 (CWE-327)#4939
Open
sebastiondev wants to merge 1 commit into1Panel-dev:v2from
Open
fix: replace unsalted MD5 password hashing with Django PBKDF2 (CWE-327)#4939sebastiondev wants to merge 1 commit into1Panel-dev:v2from
sebastiondev wants to merge 1 commit into1Panel-dev:v2from
Conversation
Replace hashlib.md5() in password_encrypt() with Django make_password() which uses PBKDF2-SHA256 with per-hash random salt by default. Add password_verify() for authentication that supports both new PBKDF2 hashes and legacy MD5 hashes (backward compatible). Add transparent upgrade: on successful login with a legacy MD5 hash, the stored hash is automatically upgraded to PBKDF2. Update login flow to use password_verify() instead of DB-level password comparison, and fix hardcoded MD5 hash check for default password detection in user profile.
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Author
|
Thanks for the bot notifications — I've addressed the missing release-note block by appending it to the PR body: The Regarding OWNERS approval — this is a security fix PR, so it's expected to require maintainer review and |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability Summary
CWE: CWE-327 — Use of a Broken or Risky Cryptographic Algorithm
Severity: High
OWASP: A02:2021 — Cryptographic Failures
Data Flow
rsa_util.decrypt)hashlib.md5()) inapps/common/utils/common.py:password_encrypt()usertablefilter()— meaning the password hash is included in the SQLWHEREclauseWhy This Matters
ALLOWED_HOSTS = ["*"], binds0.0.0.0:8080, Docker quick-start exposes port directlyadmin/MaxKB@123..) are hashed and stored asd880e722c47a34d8e9fce789fc62389d— which was hardcoded inuser.pyfor the "needs password change" check, meaning anyone can Google the hash to recover the default passwordFix Description
3 files changed, 66 insertions, 16 deletions — minimal, focused change.
apps/common/utils/common.pypassword_encrypt()now delegates todjango.contrib.auth.hashers.make_password()→ PBKDF2-SHA256 with 870,000 iterations (Django 5.2 default) and a random saltpassword_verify()function usesdjango.contrib.auth.hashers.check_password()as the primary path, with a legacy MD5 fallback for not-yet-migrated hashes_is_legacy_md5_hash()detects old 32-character hex-digest hashes (Django hashes always contain$separators)needs_password_upgrade()exposes upgrade detection for callersapps/users/serializers/login.pypassword=password_encrypt(password)in SQL — instead fetches the user by username, then callspassword_verify()to compareapps/users/serializers/user.py== "d880e722c47a34d8e9fce789fc62389d") is replaced withpassword_verify(CONFIG.get("DEFAULT_PASSWORD", "MaxKB@123.."), user.password)— works correctly regardless of hash algorithmRationale
passwordcolumnmax_length=150comfortably fits PBKDF2 output (89 characters)Test Results Summary
Functional Verification
password_encrypt()produces Django-format PBKDF2 hashes (starts withpbkdf2_sha256$)password_verify()correctly validates PBKDF2-hashed passwordspassword_verify()correctly validates legacy MD5-hashed passwords (backward compat)needs_password_upgrade()returnsTruefor MD5 hashes,Falsefor PBKDF2 hashes_is_legacy_md5_hash()correctly distinguishes 32-char hex strings from Django hashespassword_encrypt()with the same input produce different hashes (salt is working)Edge Cases Identified (Follow-up Recommendations)
password_verify()should add aNoneguard:if not hashed_password: return False==— ideally should usehmac.compare_digest()to prevent timing side-channel (low practical risk since it's a migration path)Disprove Analysis
We systematically attempted to disprove this finding:
password_encrypt/password_verifyare utility functions; login endpoint is intentionally unauthenticated. The vulnerability is in the algorithm, not access control.ALLOWED_HOSTS = ["*"],0.0.0.0:8080, no reverse proxy requireddocker run -p 8080:8080directly exposed; README says "access athttp://your_server_ip:8080"password_encryptused unsalted MD5: login verification, user creation, password change, password reset (email), password reset (phone), initial migration,set_password()model methodSECURITY.mdpoints tosupport@fit2cloud.comExploit Sketch
Scenario: Attacker obtains
usertable contents (via SQL injection, backup leak, etc.)Verdict: CONFIRMED_VALID (High Confidence)
Unambiguous CWE-327 in an internet-facing application with zero mitigating controls. Unable to find any argument to disprove the finding.
Prior Art
Similar MD5 Usage (Non-Password, Not Changed)
MD5 is used elsewhere in the codebase for non-security purposes (document hashing, cache keys, etc.). These have a different risk profile and are not addressed in this PR:
apps/common/init/init_doc.py:53— document content fingerprintingapps/common/db/search.py:31— cache key generationapps/application/serializers/application_api_key.py:54— API key from UUIDSubmitted by Sebastion. Happy to discuss or adjust anything — the goal is to help strengthen MaxKB's security posture.