diff --git a/app/__init__.py b/app/__init__.py index 88508e9..7af4080 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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", "-") ) diff --git a/app/api/__init__.py b/app/api/__init__.py index 5f036f1..4966c10 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -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 diff --git a/fpaste b/fpaste index a72af1d..c3b34d6 100755 --- a/fpaste +++ b/fpaste @@ -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, } diff --git a/tests/test_security.py b/tests/test_security.py index fb8e4e7..1e21287 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -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."""