or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

data-structures.mddev-server.mdexceptions.mdhttp-utilities.mdindex.mdmiddleware.mdrequest-response.mdrouting.mdsecurity.mdtesting.mdurl-wsgi-utils.md

security.mddocs/

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

```