🌐
GitHub
github.com › pallets › werkzeug › blob › main › src › werkzeug › security.py
werkzeug/src/werkzeug/security.py at main · pallets/werkzeug
The comprehensive WSGI web application library. Contribute to pallets/werkzeug development by creating an account on GitHub.
Author   pallets
🌐
DEV Community
dev.to › aws-builders › python-stop-storing-passwords-in-plain-text-a-guide-to-werkzeugsecurity-179i
Python - Stop storing passwords in plain text! A guide to werkzeug.security - DEV Community
January 5, 2026 - # Import generate_password_hash from werkzeug.security import generate_password_hash # Plain text password, if using Flask, it can come from any web form plain_password = "SHH_SECRET" # Generate a hashed string using default parameters hashed = generate_password_hash(plain_password) # Printing a hashed string print("Hashed String: ", hashed)
🌐
Techmonger
techmonger.github.io › 4 › secure-passwords-werkzeug
Secure Passwords in Python With Werkzeug - Tech Monger
November 26, 2017 - Learn how to store user passwords securely using hashes, salts and HMAC with werkzeug.
🌐
IBM
ibm.com › support › pages › security-bulletin-there-vulnerability-python-wheel-package-werkzeug-library-affecting-watsonx-code-assistant-prem-extensions
Security Bulletin: There is a vulnerability in Python wheel package for the Werkzeug library affecting watsonx Code Assistant On Prem Extensions
March 27, 2025 - CWE: CWE-22: Improper Limitation .../MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X) CVEID: CVE-2024-49767 DESCRIPTION: Werkzeug is a Web Server Gateway Interface web application library....
🌐
Debian
debian.org › security › 2023 › dsa-5470
Debian -- Security Information -- DSA-5470-1 python-werkzeug
Several vulnerabilities were discovered in python-werkzeug, a collection of utilities for WSGI applications.
🌐
Debian
security-tracker.debian.org › tracker › source-package › python-werkzeug
Information on source package python-werkzeug
python-werkzeug in the Package Tracking System · python-werkzeug in the Bug Tracking System · python-werkzeug source code ·
Top answer
1 of 3
1

I'm not sure I'm able to give a good answer to all your questions, but I found it interesting and took a look and here is my result.

In general, import mod.sub or from mod import sub assumes that sub is a sub-module in a mod package. However, it could also mean that sub is a field/variable declared in a mod module.

The presence of an __init.py__-file will denote that a folder is a package:

The __init__.py files are required to make Python treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. In the simplest case, __init__.py can just be an empty file, but it can also execute initialization code for the package (...).

I believe that, from werkzeug import security and import werkzeug.security both imports a module security, thus security.generate_password_hash is a known and valid attribute. Basically, from werkzeug.security import generate_password_hash imports that very attribute directly via the valid import statement.

In the Werkzeug Quickstart docs, I found the following:

Make sure to import all objects from the places the documentation suggests. It is theoretically possible in some situations to import objects from different locations but this is not supported.

Further, Werkzeug transition to 1.0 states:

Werkzeug originally had a magical import system hook that enabled everything to be imported from one module and still loading the actual implementations lazily as necessary. Unfortunately this turned out to be slow and also unreliable on alternative Python implementations and Google’s App Engine.

Starting with 0.7 we recommend against the short imports and strongly encourage starting importing from the actual implementation module. Werkzeug 1.0 will disable the magical import hook completely.

It appears that Werkzeug modifies how modules are loaded. (I speculate that this is not uncommon in big packages with contrib-content, e.g. Flask, Django; motivated by ability to lazy-load, improve performance, or manage contributed module content spread across packages.)

As you've discovered, import werkzeug does not import security from the werkzeug module, because (as far as I understand), the only submodules that will be imported as attributes are those defined on line 100 of the __init__.py:

# modules that should be imported when accessed as attributes of werkzeug
attribute_modules = frozenset(['exceptions', 'routing'])

In the same file, when looking at the Werkzeug's module(ModuleType)-class, and its __getattr__()-method:

class module(ModuleType):

    """Automatically import objects from the modules."""

    def __getattr__(self, name):
        if name in object_origins:
            module = __import__(object_origins[name], None, None, [name])
            for extra_name in all_by_module[module.__name__]:
                setattr(self, extra_name, getattr(module, extra_name))
            return getattr(module, name)
        elif name in attribute_modules:
            __import__('werkzeug.' + name)
        return ModuleType.__getattribute__(self, name)

It seems that module names in the object_origins dictionary, via definition in all_by_module, must be imported separately, and werkzeug.security is one of them.

Lastly, I think the reason for why the:

import werkzeug     
from werkzeug import security  

combination works, is that the first line does not import security, but the second one does, AND the __getattr__()-method will return modules that are explicitly imported.

Edit: this last section is not correct, tested by Filipp:

I expect that by simply doing only from werkzeug import security that still werkzeug.security.generate_password_hash() would work. (I have not tested or confirmed this)

2 of 3
1

TL;DR: import any attribute, contained in all_by_module dictionary, directly from werkzeug, i.e. from werkzeug import generate_password_hash.


Inspired by/ based on Thomas's answer, I will try to summarize answers to my own questions:

  1. Am I wrong (or lacking details) in some of my notions, concerning how import works in Python?

From where I currently stand, the short answer is NO. Though, it's good to keep in mind that import rules/mechanics could be customized on package level via __init__.py.
Further reading on topic: Python import system, official docs on importlib, Importing Python Modules article.

  1. Why import werkzeug won't give me access to werkzeug.security? My understanding is — it should import werkzeug, along with all of it's submodules/attributes.

As Thomas Fauskanger, correctly pointed out in his answer: import werkzeug does not import security from the werkzeug module, because the only submodules that will be imported as attributes — are those defined on line 100 of the Werkzeug's __init__.py (which are exceptions and routing). This assumption, could be verified by the following:

import werkzeug

werkzeug.routing # will return path to routing.py module
werkzeug.exceptions # will return path to exceptions.py module

werkzeug.security # AttributeError: module 'werkzeug' has no attribute 'security'
  1. Why import werkzeug + from werkzeug import security allows access to werkzeug.security? My understanding: it should bind two separate names (with no connections between them), as follows: werkzeug to import werkzeug (i.e. werkzeug module) and security to from werkzeug import security (i.e. security submodule of werkzeug module.

That's a tricky one. As it is indicated in Werkzeug's __init__.py, by the docstring for module's __dir__ function:

Just show what we want to show.

That's (probably) why:

import werkzeug

dir1 = dir(werkzeug)
werkzeug.security # AttributeError: module 'werkzeug' has no attribute 'security'

from werkzeug import security

dir2 = dir(werkzeug)
werkzeug.security # will return path to security.py module
# BUT!
dir1 == dir2 # True

I think, Thomas right here as well, and:

...__getattr__() method will return modules that are explicitly imported.


Conclusion (or what I have learned =):

As stated in the docstring for Werkzeug's __init__.py:

...
The majority of the functions and classes provided by Werkzeug work on the HTTP and WSGI layer. There is no useful grouping for those which is why they are all importable from "werkzeug" instead of the modules where they are implemented.
...
The implementation of a lazy-loading module in this file replaces the werkzeug package when imported from within. Attribute access to the werkzeug module will then lazily import from the modules that implement the objects.

What this means is, instead of:

from werkzeug import security
security.generate_password_hash('some_password', method='pbkdf2:sha512', salt_length=25)
# OR
import werkzeug.security
werkzeug.security.generate_password_hash('some_password', method='pbkdf2:sha512', salt_length=25)
# OR
from werkzeug.security import generate_password_hash
generate_password_hash('some_password', method='pbkdf2:sha512', salt_length=25)
# OR
import werkzeug
from werkzeug import security
werkzeug.security.generate_password_hash('some_password', method='pbkdf2:sha512', salt_length=25)

You can simply do:

from werkzeug import generate_password_hash
generate_password_hash('some_password', method='pbkdf2:sha512', salt_length=25)

You can import & use any attribute contained in all_by_module dictionary, in the same fashion.

Top answer
1 of 1
1

werkzeug.generate_password_hash wants to generate a salt value. Taking a look at the source we can see _hash_internal called with the generated salt value.

def generate_password_hash(password, method="pbkdf2:sha256", salt_length=8):
    """Hash a password with the given method and salt with a string of
    the given length. The format of the string returned includes the method
    that was used so that :func:`check_password_hash` can check the hash.

    The format for the hashed string looks like this::

        method$salt$hash

    This method can **not** generate unsalted passwords but it is possible
    to set param method='plain' in order to enforce plaintext passwords.
    If a salt is used, hmac is used internally to salt the password.

    If PBKDF2 is wanted it can be enabled by setting the method to
    ``pbkdf2:method:iterations`` where iterations is optional::

        pbkdf2:sha256:80000$salt$hash
        pbkdf2:sha256$salt$hash

    :param password: the password to hash.
    :param method: the hash method to use (one that hashlib supports). Can
                   optionally be in the format ``pbkdf2:<method>[:iterations]``
                   to enable PBKDF2.
    :param salt_length: the length of the salt in letters.
    """
    salt = gen_salt(salt_length) if method != "plain" else ""
    h, actual_method = _hash_internal(method, salt, password)
    return "%s$%s$%s" % (actual_method, salt, h)

If we call _hash_internal with your sha1 method, and provided salt/password, we get a different hash

In [83]: import werkzeug.security                                                                                                                                                     

In [84]: h, method = werkzeug.security._hash_internal('sha1', 'e40e1e9addc186828a5554a71527342c', '123456')                                                                           

In [86]: h == '784517f57fbe61179960739e29d7ae925aa4fd5b'                                                                                                                              
Out[86]: False

In [85]: h                                                                                                                                                                            
Out[85]: 'e8c2de9bdc1ab92479e3e55b608a040dad7bf656'

I think you'll need to revisit your PHP code to see how these values are generated.

EDIT: per your comment

In [138]: werkzeug.security._hash_internal('sha1', '', 'e40e1e9addc186828a5554a71527342c123456')                                                                                      
Out[138]: ('784517f57fbe61179960739e29d7ae925aa4fd5b', 'sha1')

If you want to use check_password_hash you'd have to not set the salt on your hash and instead prepend it to your password:

In [148]: werkzeug.check_password_hash('sha1$$784517f57fbe61179960739e29d7ae925aa4fd5b', 'e40e1e9addc186828a5554a71527342c123456')                                                    
Out[148]: True

If we look at the source we can see that check_password_hash extracts method, salt, and hashval out of the pwhash, then validates the hashed password matches hashval. I'm not familiar with hashing in PHP, but it appears the password was "salted" and then hashed without salt (as far as werkzeug.security is concerned anyway).

def check_password_hash(pwhash, password):
    """check a password against a given salted and hashed password value.
    In order to support unsalted legacy passwords this method supports
    plain text passwords, md5 and sha1 hashes (both salted and unsalted).

    Returns `True` if the password matched, `False` otherwise.

    :param pwhash: a hashed string like returned by
                   :func:`generate_password_hash`.
    :param password: the plaintext password to compare against the hash.
    """
    if pwhash.count("$") < 2:
        return False
    method, salt, hashval = pwhash.split("$", 2)
    return safe_str_cmp(_hash_internal(method, salt, password)[0], hashval)
🌐
GitHub
github.com › AnataarXVI › Werkzeug-Cracker
GitHub - AnataarXVI/Werkzeug-Cracker: Werkzeug password cracker · GitHub
>>> from werkzeug.security import generate_password_hash >>> hash = generate_password_hash("password", method='pbkdf2:sha256', salt_length=8) >>> hash 'pbkdf2:sha256:260000$3LESq315$6f074a3d958ad256ced33cc72dfb79fda306ea53eb4d171d4c1bee4881e778c1'
Starred by 19 users
Forked by 2 users
Languages   Python
Find elsewhere
🌐
Invicti
invicti.com › web-vulnerability-scanner › vulnerabilities › version-disclosure-werkzeug-python-wsgi-library
Version Disclosure (Werkzeug Python WSGI Library)
This information can help an attacker gain a greater understanding of the systems in use and potentially develop further attacks targeted at the specific version of Werkzeug. ... An attacker might use the disclosed information to harvest specific security vulnerabilities for the version identified.
🌐
DEV Community
dev.to › goke › securing-your-flask-application-hashing-passwords-tutorial-2f0p
Securing Your Flask Application: Hashing Passwords Tutorial - DEV Community
May 26, 2023 - By utilizing SQLAlchemy, we can interact with the database using Python objects. Create a new file named models.py in your project directory. This file will contain the User model definition. Open models.py in a text editor and add the following code: from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)
🌐
Linux Security
linuxsecurity.com › home › advisories › mageia linux distribution - security advisories › mageia: 2024-0234 critical: python-werkzeug code execution advisory
Mageia: 2024-0234 Critical: Python-Werkzeug Code Execution
June 24, 2024 - MGASA-2024-0234 - Updated ... security Affected Mageia releases: 9 CVE: CVE-2024-34069 Werkzeug is a comprehensive WSGI web application library....
🌐
Medium
medium.com › @rajk88 › understanding-werkzeug-exploitation-for-penetration-testers-b38f4502469b
Understanding Werkzeug Exploitation for Penetration Testers | by Raj K | Medium
December 7, 2024 - Werkzeug is a comprehensive WSGI ... and flexibility. While Werkzeug is powerful, its features can also inadvertently expose vulnerabilities if misconfigured or improperly secured....
🌐
PyPI
pypi.org › project › Werkzeug
Werkzeug · PyPI
Publisher: publish.yaml on pallets/werkzeug Attestations: Values shown here reflect the state when the release was signed and may no longer be current. ... AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page
      » pip install Werkzeug
    
Published   Apr 02, 2026
Version   3.1.8
🌐
Stack Overflow
stackoverflow.com › questions › 71432845 › recreating-pythons-werkzeug-security-generate-password-hash-in-c-sharp
recreating pythons werkzeug.security generate_password_hash in C# - Stack Overflow
So to summarize: werkzeug.security.generate_password_hash only guarantee that generated hash can be validated by check_password_hash, and no more. You simply cannot(or not supposed to) try to generate same hash by other libraries or languages. If you really want to compare the hashing algorithm in python and C#, please post another question(or update this question) that compares underlying algorithm(hashlib.pbkdf2_hmac which allow specifying salt as parameter) with C# version.
🌐
Snyk
security.snyk.io › snyk vulnerability database › linux › rhel
python-werkzeug vulnerabilities | Snyk
Known vulnerabilities in the python-werkzeug package. This does not include vulnerabilities belonging to this package’s dependencies. Snyk's AI Trust Platform automatically finds the best upgrade path and integrates with your development workflows. Secure your code at zero cost.