refactor: code consistency and best practices

- add type hints to error handlers in app/__init__.py
- add docstrings to nested callback functions
- remove deprecated X-XSS-Protection header (superseded by CSP)
- fix typo in cleanup log message (entr(ies) -> entries)
- standardize loop variable naming in fpaste CLI
- update test for intentional header removal
This commit is contained in:
Username
2025-12-22 00:25:18 +01:00
parent 028367d803
commit 680b068c00
4 changed files with 30 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import sys
import uuid
from flask import Flask, Response, g, request
from werkzeug.exceptions import HTTPException
from app.config import VERSION, config
@@ -39,12 +40,14 @@ def setup_security_headers(app: Flask) -> None:
@app.after_request
def add_security_headers(response: Response) -> Response:
"""Apply security headers to response.
Headers follow OWASP recommendations for API security.
"""
# Prevent MIME type sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Prevent clickjacking
response.headers["X-Frame-Options"] = "DENY"
# XSS protection (legacy but still useful)
response.headers["X-XSS-Protection"] = "1; mode=block"
# Referrer policy
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# Content Security Policy (restrictive for API)
@@ -64,8 +67,8 @@ def setup_request_id(app: Flask) -> None:
"""Add request ID tracking for log correlation and tracing."""
@app.before_request
def assign_request_id():
# Use incoming X-Request-ID from proxy, or generate a new one
def assign_request_id() -> None:
"""Assign unique request ID from header or generate new UUID."""
request_id = request.headers.get("X-Request-ID", "").strip()
if not request_id:
request_id = str(uuid.uuid4())
@@ -73,7 +76,7 @@ def setup_request_id(app: Flask) -> None:
@app.after_request
def add_request_id_header(response: Response) -> Response:
# Echo request ID back to client for tracing
"""Echo request ID in response header and log access."""
request_id = getattr(g, "request_id", None)
if request_id:
response.headers["X-Request-ID"] = request_id
@@ -90,11 +93,12 @@ def setup_request_id(app: Flask) -> None:
def setup_error_handlers(app: Flask) -> None:
"""Register global error handlers."""
"""Register global error handlers with JSON responses."""
import json
@app.errorhandler(400)
def bad_request(error):
def bad_request(error: HTTPException) -> Response:
"""Handle 400 Bad Request errors."""
app.logger.warning("Bad request: %s [rid=%s]", request.path, getattr(g, "request_id", "-"))
return Response(
json.dumps({"error": "Bad request"}),
@@ -103,7 +107,8 @@ def setup_error_handlers(app: Flask) -> None:
)
@app.errorhandler(404)
def not_found(error):
def not_found(error: HTTPException) -> Response:
"""Handle 404 Not Found errors."""
return Response(
json.dumps({"error": "Not found"}),
status=404,
@@ -111,7 +116,8 @@ def setup_error_handlers(app: Flask) -> None:
)
@app.errorhandler(429)
def rate_limit_exceeded(error):
def rate_limit_exceeded(error: HTTPException) -> Response:
"""Handle 429 Too Many Requests errors."""
app.logger.warning(
"Rate limit exceeded: %s from %s [rid=%s]",
request.path,
@@ -125,7 +131,8 @@ def setup_error_handlers(app: Flask) -> None:
)
@app.errorhandler(500)
def internal_error(error):
def internal_error(error: HTTPException) -> Response:
"""Handle 500 Internal Server errors."""
app.logger.error(
"Internal error: %s - %s [rid=%s]",
request.path,
@@ -139,7 +146,8 @@ def setup_error_handlers(app: Flask) -> None:
)
@app.errorhandler(Exception)
def handle_exception(error):
def handle_exception(error: Exception) -> Response:
"""Handle unhandled exceptions with generic 500 response."""
app.logger.exception(
"Unhandled exception: %s [rid=%s]", str(error), getattr(g, "request_id", "-")
)

View File

@@ -59,7 +59,7 @@ def run_scheduled_cleanup():
count = cleanup_rate_limits()
if count > 0:
current_app.logger.info(f"Cleaned up {count} rate limit entr(ies)")
current_app.logger.info(f"Cleaned up {count} rate limit entries")
from app.api import routes # noqa: E402, F401

14
fpaste
View File

@@ -640,7 +640,7 @@ def cmd_delete(args: argparse.Namespace, config: dict[str, Any]) -> None:
delete_all = getattr(args, "all", False)
confirm_count = getattr(args, "confirm", None)
paste_ids = [pid.split("/")[-1] for pid in (args.ids or [])]
paste_ids = [paste_id.split("/")[-1] for paste_id in (args.ids or [])]
# Validate arguments
if delete_all and paste_ids:
@@ -890,20 +890,20 @@ def cmd_export(args: argparse.Namespace, config: dict[str, Any]) -> None:
exported, skipped, errors = 0, 0, 0
manifest: list[dict[str, Any]] = []
for p in pastes:
paste_id = p["id"]
mime_type = p.get("mime_type", "application/octet-stream")
for paste in pastes:
paste_id = paste["id"]
mime_type = paste.get("mime_type", "application/octet-stream")
if not args.quiet:
print(f"exporting {paste_id}...", end=" ", file=sys.stderr)
if p.get("burn_after_read"):
if paste.get("burn_after_read"):
if not args.quiet:
print("skipped (burn-after-read)", file=sys.stderr)
skipped += 1
continue
if p.get("password_protected"):
if paste.get("password_protected"):
if not args.quiet:
print("skipped (password-protected)", file=sys.stderr)
skipped += 1
@@ -939,7 +939,7 @@ def cmd_export(args: argparse.Namespace, config: dict[str, Any]) -> None:
"filename": filename,
"mime_type": mime_type,
"size": len(content),
"created_at": p.get("created_at"),
"created_at": paste.get("created_at"),
"decrypted": decrypted,
"encrypted": paste_id in keys and not decrypted,
}

View File

@@ -16,10 +16,10 @@ class TestSecurityHeaders:
response = client.get("/")
assert response.headers.get("X-Frame-Options") == "DENY"
def test_x_xss_protection(self, client):
"""X-XSS-Protection header is set."""
def test_x_xss_protection_not_present(self, client):
"""X-XSS-Protection header is not set (deprecated, superseded by CSP)."""
response = client.get("/")
assert response.headers.get("X-XSS-Protection") == "1; mode=block"
assert response.headers.get("X-XSS-Protection") is None
def test_content_security_policy(self, client):
"""Content-Security-Policy header is set."""