0
# Security Utilities
1
2
Security functions for password hashing, secure filename handling, and safe filesystem operations. These utilities provide essential security primitives for web applications including cryptographically secure password storage and path traversal protection.
3
4
## Capabilities
5
6
### Password Security
7
8
Functions for securely hashing and verifying passwords using modern cryptographic methods.
9
10
```python { .api }
11
def generate_password_hash(password, method="scrypt", salt_length=16):
12
"""
13
Securely hash a password for storage using strong key derivation functions.
14
15
Parameters:
16
- password: Plaintext password to hash
17
- method: Key derivation method and parameters
18
- "scrypt" (default): scrypt:n:r:p format, default scrypt:32768:8:1
19
- "pbkdf2": pbkdf2:hash_method:iterations format, default pbkdf2:sha256:1000000
20
- salt_length: Number of characters for random salt
21
22
Returns:
23
Hashed password string in format: method$salt$hash
24
25
Examples:
26
- generate_password_hash("secret") # Uses scrypt with default params
27
- generate_password_hash("secret", "pbkdf2:sha256:1200000")
28
- generate_password_hash("secret", "scrypt:65536:16:2") # Custom scrypt params
29
"""
30
31
def check_password_hash(pwhash, password):
32
"""
33
Verify a password against a stored hash.
34
35
Parameters:
36
- pwhash: Previously generated hash from generate_password_hash()
37
- password: Plaintext password to verify
38
39
Returns:
40
True if password matches hash, False otherwise
41
42
Note: Uses constant-time comparison to prevent timing attacks.
43
"""
44
45
def gen_salt(length):
46
"""
47
Generate a cryptographically secure random salt.
48
49
Parameters:
50
- length: Length of salt in characters
51
52
Returns:
53
Random salt string using characters from SALT_CHARS
54
55
Raises:
56
ValueError: If length is less than 1
57
"""
58
59
# Constants
60
SALT_CHARS: str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
61
DEFAULT_PBKDF2_ITERATIONS: int = 1_000_000
62
```
63
64
### Filesystem Security
65
66
Functions for safe filesystem operations to prevent path traversal and other file-related attacks.
67
68
```python { .api }
69
def secure_filename(filename):
70
"""
71
Make a filename safe for filesystem storage by removing dangerous characters.
72
73
Parameters:
74
- filename: Original filename (may contain unsafe characters)
75
76
Returns:
77
ASCII-only filename safe for filesystem storage
78
79
Note: May return empty string for extremely unsafe filenames.
80
It's your responsibility to handle empty results and ensure uniqueness.
81
82
Examples:
83
- secure_filename("My cool movie.mov") → "My_cool_movie.mov"
84
- secure_filename("../../../etc/passwd") → "etc_passwd"
85
- secure_filename("file with unicode.txt") → "file_with_unicode.txt"
86
"""
87
88
def safe_join(directory, *pathnames):
89
"""
90
Safely join path components to prevent directory traversal attacks.
91
92
Parameters:
93
- directory: Trusted base directory path
94
- *pathnames: Untrusted path components to join
95
96
Returns:
97
Safe combined path or None if any component is unsafe
98
99
Security checks:
100
- Prevents ".." directory traversal
101
- Prevents absolute paths in pathnames
102
- Prevents OS-specific path separators in unsafe contexts
103
- Normalizes paths to prevent bypass attempts
104
105
Examples:
106
- safe_join("/var/www", "uploads", "file.txt") → "/var/www/uploads/file.txt"
107
- safe_join("/var/www", "../etc/passwd") → None (unsafe)
108
- safe_join("/var/www", "/etc/passwd") → None (absolute path)
109
"""
110
```
111
112
## Usage Examples
113
114
### Password Hashing and Verification
115
116
```python
117
from werkzeug.security import generate_password_hash, check_password_hash
118
119
class User:
120
def __init__(self, username, password):
121
self.username = username
122
self.password_hash = generate_password_hash(password)
123
124
def check_password(self, password):
125
return check_password_hash(self.password_hash, password)
126
127
# User registration
128
def register_user(username, password):
129
# Validate password strength first (your code)
130
if len(password) < 8:
131
raise ValueError("Password must be at least 8 characters")
132
133
# Hash password securely
134
user = User(username, password)
135
136
# Store user in database (your code)
137
save_user_to_database(user)
138
139
return user
140
141
# User login
142
def authenticate_user(username, password):
143
user = get_user_from_database(username)
144
145
if user and user.check_password(password):
146
return user
147
148
return None
149
150
# Example usage
151
user = register_user("john", "mySecretPassword123")
152
print(user.password_hash) # scrypt:32768:8:1$randomSalt$hashedPassword
153
154
# Later authentication
155
if authenticate_user("john", "mySecretPassword123"):
156
print("Login successful")
157
else:
158
print("Invalid credentials")
159
```
160
161
### Advanced Password Hashing Options
162
163
```python
164
from werkzeug.security import generate_password_hash, check_password_hash
165
166
# Using different hashing methods
167
def hash_with_different_methods(password):
168
# Default scrypt (recommended for new applications)
169
scrypt_hash = generate_password_hash(password)
170
print(f"Scrypt: {scrypt_hash}")
171
172
# PBKDF2 with SHA-256 (for compatibility/regulations requiring PBKDF2)
173
pbkdf2_hash = generate_password_hash(password, method="pbkdf2")
174
print(f"PBKDF2: {pbkdf2_hash}")
175
176
# Custom PBKDF2 iterations
177
strong_pbkdf2 = generate_password_hash(password, method="pbkdf2:sha256:1500000")
178
print(f"Strong PBKDF2: {strong_pbkdf2}")
179
180
# Custom scrypt parameters (n=65536, r=16, p=2)
181
strong_scrypt = generate_password_hash(password, method="scrypt:65536:16:2")
182
print(f"Strong Scrypt: {strong_scrypt}")
183
184
# Longer salt
185
long_salt_hash = generate_password_hash(password, salt_length=32)
186
print(f"Long salt: {long_salt_hash}")
187
188
return [scrypt_hash, pbkdf2_hash, strong_pbkdf2, strong_scrypt, long_salt_hash]
189
190
# Verify all hashes work
191
password = "testPassword123"
192
hashes = hash_with_different_methods(password)
193
194
for i, hash_value in enumerate(hashes):
195
if check_password_hash(hash_value, password):
196
print(f"Hash {i+1} verified successfully")
197
else:
198
print(f"Hash {i+1} verification failed")
199
```
200
201
### Password Migration Strategy
202
203
```python
204
from werkzeug.security import generate_password_hash, check_password_hash
205
206
class UserAccount:
207
def __init__(self, username, password_hash):
208
self.username = username
209
self.password_hash = password_hash
210
211
def verify_and_upgrade_password(self, password):
212
"""Verify password and upgrade hash if using old method."""
213
214
if not check_password_hash(self.password_hash, password):
215
return False
216
217
# Check if using old/weak hashing method
218
if self.needs_password_upgrade():
219
# Rehash with current strong defaults
220
self.password_hash = generate_password_hash(password)
221
# Save updated hash to database
222
self.save_to_database()
223
print(f"Password hash upgraded for user {self.username}")
224
225
return True
226
227
def needs_password_upgrade(self):
228
"""Check if password hash should be upgraded."""
229
# Upgrade if using old PBKDF2 with low iterations
230
if self.password_hash.startswith('pbkdf2:'):
231
parts = self.password_hash.split('$')[0].split(':')
232
if len(parts) >= 3:
233
iterations = int(parts[2])
234
if iterations < 1_000_000: # Below current minimum
235
return True
236
237
# Upgrade if using deprecated methods
238
if self.password_hash.startswith('md5') or self.password_hash.startswith('sha1'):
239
return True
240
241
return False
242
243
def save_to_database(self):
244
"""Save user to database (implement your database logic)."""
245
pass
246
247
# Example migration during login
248
def login_with_migration(username, password):
249
user = get_user_from_database(username)
250
251
if user and user.verify_and_upgrade_password(password):
252
return user
253
254
return None
255
```
256
257
### Secure File Handling
258
259
```python
260
from werkzeug.security import secure_filename, safe_join
261
from werkzeug.utils import secure_filename # Alternative import
262
import os
263
264
class FileUploadHandler:
265
def __init__(self, upload_directory):
266
self.upload_directory = os.path.abspath(upload_directory)
267
os.makedirs(self.upload_directory, exist_ok=True)
268
269
def save_uploaded_file(self, file_storage, subdirectory=None):
270
"""Safely save an uploaded file."""
271
272
if not file_storage.filename:
273
raise ValueError("No filename provided")
274
275
# Secure the filename
276
filename = secure_filename(file_storage.filename)
277
278
if not filename:
279
# Generate fallback filename if original was completely unsafe
280
import uuid
281
ext = os.path.splitext(file_storage.filename)[1]
282
filename = f"{uuid.uuid4().hex}{ext}"
283
filename = secure_filename(filename) # Double-check
284
285
# Build safe path
286
if subdirectory:
287
# Use safe_join to prevent directory traversal
288
target_dir = safe_join(self.upload_directory, subdirectory)
289
if target_dir is None:
290
raise ValueError(f"Unsafe subdirectory: {subdirectory}")
291
else:
292
target_dir = self.upload_directory
293
294
# Ensure target directory exists
295
os.makedirs(target_dir, exist_ok=True)
296
297
# Create final safe path
298
file_path = safe_join(target_dir, filename)
299
if file_path is None:
300
raise ValueError(f"Unsafe filename: {filename}")
301
302
# Handle filename conflicts
303
file_path = self.get_unique_filename(file_path)
304
305
# Save file
306
file_storage.save(file_path)
307
308
# Return relative path from upload directory
309
return os.path.relpath(file_path, self.upload_directory)
310
311
def get_unique_filename(self, file_path):
312
"""Generate unique filename if file exists."""
313
if not os.path.exists(file_path):
314
return file_path
315
316
base, ext = os.path.splitext(file_path)
317
counter = 1
318
319
while True:
320
new_path = f"{base}_{counter}{ext}"
321
if not os.path.exists(new_path):
322
return new_path
323
counter += 1
324
325
def serve_file(self, requested_path):
326
"""Safely serve a file from the upload directory."""
327
328
# Use safe_join to prevent directory traversal
329
file_path = safe_join(self.upload_directory, requested_path)
330
331
if file_path is None:
332
raise ValueError("Unsafe file path")
333
334
if not os.path.exists(file_path):
335
raise FileNotFoundError("File not found")
336
337
# Additional security: ensure file is within upload directory
338
if not file_path.startswith(self.upload_directory):
339
raise ValueError("File outside upload directory")
340
341
return file_path
342
343
# Example usage
344
upload_handler = FileUploadHandler('/var/uploads')
345
346
# Test secure filename
347
def test_secure_filenames():
348
test_cases = [
349
"normal_file.txt",
350
"file with spaces.pdf",
351
"../../../etc/passwd",
352
"con.txt", # Windows reserved name
353
"file<>with|invalid:chars.doc",
354
"résumé.pdf", # Unicode characters
355
".hidden_file",
356
"file.tar.gz",
357
]
358
359
for original in test_cases:
360
secured = secure_filename(original)
361
print(f"'{original}' → '{secured}'")
362
363
# Test safe path joining
364
def test_safe_joining():
365
base_dir = "/var/uploads"
366
367
test_cases = [
368
["user1", "document.pdf"], # Safe
369
["user1", "../../../etc/passwd"], # Unsafe - traversal
370
["/etc/passwd"], # Unsafe - absolute
371
["user1", "subdir", "file.txt"], # Safe - nested
372
["user1", ""], # Safe - empty
373
["user1", ".."], # Unsafe - parent
374
]
375
376
for pathnames in test_cases:
377
result = safe_join(base_dir, *pathnames)
378
safe_status = "SAFE" if result else "UNSAFE"
379
print(f"{pathnames} → {result} ({safe_status})")
380
381
if __name__ == '__main__':
382
test_secure_filenames()
383
print()
384
test_safe_joining()
385
```
386
387
### Complete Web Application Security Example
388
389
```python
390
from werkzeug.wrappers import Request, Response
391
from werkzeug.security import generate_password_hash, check_password_hash, safe_join
392
from werkzeug.utils import secure_filename
393
from werkzeug.exceptions import BadRequest, Forbidden
394
import os
395
import json
396
397
class SecureWebApp:
398
def __init__(self):
399
self.users = {} # In production, use proper database
400
self.upload_dir = './secure_uploads'
401
os.makedirs(self.upload_dir, exist_ok=True)
402
403
def register(self, request):
404
"""Secure user registration."""
405
if not request.is_json:
406
raise BadRequest("Content-Type must be application/json")
407
408
data = request.get_json()
409
username = data.get('username', '').strip()
410
password = data.get('password', '')
411
412
# Basic validation
413
if len(username) < 3:
414
return Response('{"error": "Username too short"}', status=400)
415
416
if len(password) < 8:
417
return Response('{"error": "Password too weak"}', status=400)
418
419
if username in self.users:
420
return Response('{"error": "Username already exists"}', status=409)
421
422
# Secure password hashing
423
password_hash = generate_password_hash(password)
424
425
# Store user (in production, use proper database)
426
self.users[username] = {
427
'password_hash': password_hash,
428
'files': []
429
}
430
431
return Response('{"message": "Registration successful"}',
432
mimetype='application/json')
433
434
def login(self, request):
435
"""Secure user authentication."""
436
if not request.is_json:
437
raise BadRequest("Content-Type must be application/json")
438
439
data = request.get_json()
440
username = data.get('username', '')
441
password = data.get('password', '')
442
443
user = self.users.get(username)
444
445
# Use constant-time comparison
446
if user and check_password_hash(user['password_hash'], password):
447
# In production, use proper session management
448
return Response('{"message": "Login successful"}',
449
mimetype='application/json')
450
451
# Don't leak information about whether username exists
452
return Response('{"error": "Invalid credentials"}', status=401)
453
454
def upload_file(self, request):
455
"""Secure file upload."""
456
if 'file' not in request.files:
457
raise BadRequest("No file provided")
458
459
uploaded_file = request.files['file']
460
username = request.form.get('username', '') # In production, use sessions
461
462
if not username or username not in self.users:
463
raise Forbidden("Authentication required")
464
465
if not uploaded_file.filename:
466
raise BadRequest("No filename provided")
467
468
# Secure filename
469
filename = secure_filename(uploaded_file.filename)
470
471
if not filename:
472
raise BadRequest("Invalid filename")
473
474
# Create user subdirectory safely
475
user_dir = safe_join(self.upload_dir, username)
476
if user_dir is None:
477
raise BadRequest("Invalid username")
478
479
os.makedirs(user_dir, exist_ok=True)
480
481
# Create safe file path
482
file_path = safe_join(user_dir, filename)
483
if file_path is None:
484
raise BadRequest("Invalid file path")
485
486
# Save file
487
uploaded_file.save(file_path)
488
489
# Track user's files
490
relative_path = os.path.join(username, filename)
491
self.users[username]['files'].append(relative_path)
492
493
return Response(f'{{"message": "File {filename} uploaded successfully"}}',
494
mimetype='application/json')
495
496
def get_file(self, request, filepath):
497
"""Secure file serving."""
498
username = request.args.get('username', '') # In production, use sessions
499
500
if not username or username not in self.users:
501
raise Forbidden("Authentication required")
502
503
# Verify file belongs to user
504
if filepath not in self.users[username]['files']:
505
raise Forbidden("File not accessible")
506
507
# Safe file path construction
508
full_path = safe_join(self.upload_dir, filepath)
509
if full_path is None or not os.path.exists(full_path):
510
raise BadRequest("File not found")
511
512
# Additional security check
513
if not full_path.startswith(os.path.abspath(self.upload_dir)):
514
raise Forbidden("Access denied")
515
516
# In production, use proper file serving (X-Sendfile, etc.)
517
with open(full_path, 'rb') as f:
518
content = f.read()
519
520
return Response(content, mimetype='application/octet-stream')
521
522
def __call__(self, environ, start_response):
523
"""WSGI application."""
524
request = Request(environ)
525
526
try:
527
if request.path == '/register' and request.method == 'POST':
528
response = self.register(request)
529
elif request.path == '/login' and request.method == 'POST':
530
response = self.login(request)
531
elif request.path == '/upload' and request.method == 'POST':
532
response = self.upload_file(request)
533
elif request.path.startswith('/files/'):
534
filepath = request.path[7:] # Remove '/files/'
535
response = self.get_file(request, filepath)
536
else:
537
response = Response('Not Found', status=404)
538
539
except (BadRequest, Forbidden) as e:
540
response = Response(str(e), status=e.code)
541
except Exception as e:
542
# Don't leak internal errors
543
response = Response('Internal Server Error', status=500)
544
545
return response(environ, start_response)
546
547
if __name__ == '__main__':
548
from werkzeug.serving import run_simple
549
550
app = SecureWebApp()
551
print("Secure web app running on http://localhost:8000")
552
print("\nExample usage:")
553
print("POST /register with JSON: {'username': 'test', 'password': 'password123'}")
554
print("POST /login with JSON: {'username': 'test', 'password': 'password123'}")
555
print("POST /upload with form data: file + username")
556
print("GET /files/username/filename.txt?username=test")
557
558
run_simple('localhost', 8000, app, use_reloader=True)
559
```
560
561
### Security Best Practices
562
563
```python
564
from werkzeug.security import generate_password_hash, check_password_hash, safe_join, secure_filename
565
import secrets
566
import hmac
567
568
class SecurityBestPractices:
569
"""Examples of security best practices using Werkzeug utilities."""
570
571
@staticmethod
572
def strong_password_policy(password):
573
"""Implement strong password policy."""
574
if len(password) < 12:
575
return False, "Password must be at least 12 characters"
576
577
if not any(c.isupper() for c in password):
578
return False, "Password must contain uppercase letters"
579
580
if not any(c.islower() for c in password):
581
return False, "Password must contain lowercase letters"
582
583
if not any(c.isdigit() for c in password):
584
return False, "Password must contain numbers"
585
586
if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
587
return False, "Password must contain special characters"
588
589
return True, "Password meets requirements"
590
591
@staticmethod
592
def secure_session_token():
593
"""Generate cryptographically secure session token."""
594
return secrets.token_urlsafe(32)
595
596
@staticmethod
597
def constant_time_compare(a, b):
598
"""Use constant-time comparison for security-sensitive data."""
599
return hmac.compare_digest(str(a), str(b))
600
601
@staticmethod
602
def validate_file_upload(file_storage, allowed_extensions=None, max_size=None):
603
"""Comprehensive file upload validation."""
604
if not file_storage.filename:
605
raise ValueError("No filename provided")
606
607
# Secure filename
608
filename = secure_filename(file_storage.filename)
609
if not filename:
610
raise ValueError("Invalid filename")
611
612
# Check file extension
613
if allowed_extensions:
614
ext = filename.rsplit('.', 1)[-1].lower()
615
if ext not in allowed_extensions:
616
raise ValueError(f"File type .{ext} not allowed")
617
618
# Check file size
619
if max_size:
620
file_storage.seek(0, 2) # Seek to end
621
size = file_storage.tell()
622
file_storage.seek(0) # Reset position
623
624
if size > max_size:
625
raise ValueError(f"File too large ({size} bytes)")
626
627
return filename
628
629
@staticmethod
630
def secure_file_path(base_dir, *path_components):
631
"""Create secure file path with multiple safety checks."""
632
# Initial safe join
633
path = safe_join(base_dir, *path_components)
634
if path is None:
635
return None
636
637
# Ensure path is within base directory
638
abs_base = os.path.abspath(base_dir)
639
abs_path = os.path.abspath(path)
640
641
if not abs_path.startswith(abs_base + os.sep):
642
return None
643
644
return path
645
646
# Example usage of security best practices
647
def secure_application_example():
648
security = SecurityBestPractices()
649
650
# Test password policy
651
passwords = ["weak", "StrongPassword123!", "short"]
652
for pwd in passwords:
653
valid, msg = security.strong_password_policy(pwd)
654
print(f"'{pwd}': {msg}")
655
656
# Generate secure tokens
657
token = security.secure_session_token()
658
print(f"Session token: {token}")
659
660
# File upload validation
661
class MockFileStorage:
662
def __init__(self, filename, size=1000):
663
self.filename = filename
664
self._size = size
665
self._pos = 0
666
667
def seek(self, pos, whence=0):
668
if whence == 2: # SEEK_END
669
self._pos = self._size
670
else:
671
self._pos = pos
672
673
def tell(self):
674
return self._pos
675
676
# Test file validation
677
test_files = [
678
("document.pdf", 1000),
679
("script.exe", 500),
680
("large_file.jpg", 5000000),
681
]
682
683
allowed_extensions = {'pdf', 'jpg', 'png', 'txt'}
684
max_size = 1024 * 1024 # 1MB
685
686
for filename, size in test_files:
687
mock_file = MockFileStorage(filename, size)
688
try:
689
secure_name = security.validate_file_upload(
690
mock_file, allowed_extensions, max_size
691
)
692
print(f"'{filename}' → '{secure_name}' (valid)")
693
except ValueError as e:
694
print(f"'{filename}' → Error: {e}")
695
696
if __name__ == '__main__':
697
secure_application_example()
698
```