fix(security): replace unsalted MD5 password hashing with PBKDF2 (CWE-327/CWE-916)#5212
Open
sebastiondev wants to merge 1 commit into1Panel-dev:v2from
Open
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.
|
Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
|
[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 |
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.
Summary
Replace unsalted MD5 password hashing with Django's PBKDF2-SHA256 (
make_password/check_password) for all user account passwords, while preserving login compatibility for any existing MD5-hashed passwords through transparent rehashing on next successful login.Vulnerability
password_encrypt()inapps/common/utils/common.pyapps/users/migrations/0001_initial.py), user creation, password reset, profile update — and the login flow inapps/users/serializers/login.pythat comparedpassword=password_encrypt(password)directly in the DB query.The previous implementation was:
This is unsalted MD5 with no work factor. Concretely:
password_encrypt('MaxKB@123..'), producing the deterministic hashd880e722c47a34d8e9fce789fc62389d— a value previously hard-coded inUserProfileSerializerto detect "still using default password". Identical passwords across users produce identical hashes.usertable (SQL injection, backup leak, ops insider, dependency RCE, etc.) yields hashes that are trivially reversed via rainbow tables / GPU brute force at billions of guesses per second.The login serializer also filtered users by
password=password_encrypt(password), which both enforces the unsalted-hash design (you can't filter by a salted hash) and mixes the secret comparison into a SQLWHEREclause rather than a constant-time compare.Fix
apps/common/utils/common.py:password_encrypt()now delegates to Django'smake_password()(PBKDF2-SHA256, per-password salt, ~600k iterations by default in modern Django).password_verify()that first callscheck_password()(handles PBKDF2 and any future Django hasher) and falls back to a constant-shape MD5 check only when the stored value matches the legacy 32-hex-char shape.needs_password_upgrade()to detect legacy hashes for transparent rehashing.apps/users/serializers/login.py:User.objects.filter(username=..., password=password_encrypt(...))lookup with a username lookup followed bypassword_verify(password, user.password).user.save(update_fields=['password'])).apps/users/serializers/user.py:user.password == 'd880e722c47a34d8e9fce789fc62389d'(used to flag "default password still set") withpassword_verify(CONFIG.get('DEFAULT_PASSWORD', 'MaxKB@123..'), user.password), which works for both legacy and PBKDF2 hashes.All write-side callers of
password_encrypt()(creation, reset, profile update, etc.) automatically store PBKDF2 output without further changes.What was tested
make_password()/check_password()round-trip for a sample password produces a PBKDF2 hash and verifies correctly._is_legacy_md5_hash()accepts a 32-hex-char string and rejects PBKDF2-formatted strings (which contain$separators).pbkdf2_sha256$..., login succeeds viacheck_password.needs_password_upgrade()triggers and the row is rewritten to PBKDF2; subsequent logins go through the PBKDF2 path.password_verifyreturnsFalse,_handle_failed_loginruns as before; lockout/captcha logic is unchanged.hashlib.md5for password storage; the only remaining MD5 use is the explicit legacy-fallback helper.Security analysis
usertable reveals MD5 hashes that can be cracked offline at GPU speeds; identical passwords across users collide;MaxKB@123..reduces to a known constant.WHEREclause; verification uses Django's constant-timecheck_password.Adversarial review
Before submitting we tried to disprove this. The main counter-argument is "you need DB access to exploit it, and DB access is already game over." That's not equivalent: read-only DB exposure (backup leak,
SELECT-only SQLi, log scraping, replica access) does not by itself give an attacker the user's plaintext credential — but unsalted MD5 does. Recovered plaintext enables credential stuffing on third-party services that the application boundary cannot protect, which is the specific harm CWE-916 captures. We also checked whether Django or the framework was already upgrading these hashes transparently — it isn't, because the codebase bypasses Django's auth stack and stores raw MD5 in a plainCharField.cc @lewiswigmore