Centralize and Validate Request Size Limits to Prevent Misconfiguration and Strengthen HTTP Boundary Enforcement
This patch introduces a structured and validated mechanism for resolving request size limits in the Flask request layer. Previously, request size limits (MAX_CONTENT_LENGTH, MAX_FORM_MEMORY_SIZE, MAX_FORM_PARTS) were accessed directly from application configuration in multiple properties without centralized validation or invariant enforcement. This allowed silent misconfiguration and scattered boundary policy logic.
This commit is contained in:
parent
d98eb69a35
commit
b8fbbc5b54
1 changed files with 54 additions and 13 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
@ -15,6 +16,31 @@ if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class RequestLimits:
|
||||||
|
"""Validated view of configured request size limits.
|
||||||
|
|
||||||
|
This centralizes basic sanity checks for limits that control how much
|
||||||
|
data Flask will accept and parse from a client request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
max_content_length: int | None = None
|
||||||
|
max_form_memory_size: int = 500_000
|
||||||
|
max_form_parts: int = 1_000
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.max_content_length is not None and self.max_content_length < 0:
|
||||||
|
raise ValueError("MAX_CONTENT_LENGTH must be non-negative or None.")
|
||||||
|
|
||||||
|
if self.max_form_memory_size < 0:
|
||||||
|
raise ValueError("MAX_FORM_MEMORY_SIZE must be non-negative.")
|
||||||
|
|
||||||
|
if self.max_form_parts is not None and self.max_form_parts < 1: # type: ignore[redundant-expr]
|
||||||
|
# NOTE: max_form_parts is annotated as int, but allow a defensive
|
||||||
|
# check in case callers or future changes make it optional.
|
||||||
|
raise ValueError("MAX_FORM_PARTS must be at least 1.")
|
||||||
|
|
||||||
|
|
||||||
class Request(RequestBase):
|
class Request(RequestBase):
|
||||||
"""The request object used by default in Flask. Remembers the
|
"""The request object used by default in Flask. Remembers the
|
||||||
matched endpoint and view arguments.
|
matched endpoint and view arguments.
|
||||||
|
|
@ -56,6 +82,27 @@ class Request(RequestBase):
|
||||||
_max_form_memory_size: int | None = None
|
_max_form_memory_size: int | None = None
|
||||||
_max_form_parts: int | None = None
|
_max_form_parts: int | None = None
|
||||||
|
|
||||||
|
def _get_limits(self) -> RequestLimits:
|
||||||
|
"""Return validated request limits derived from the current app config.
|
||||||
|
|
||||||
|
Falls back to Werkzeug's defaults when no Flask app is active, in
|
||||||
|
order to preserve existing behaviour for non-Flask usage.
|
||||||
|
"""
|
||||||
|
if not current_app:
|
||||||
|
# When there is no current app, use the underlying Werkzeug limits.
|
||||||
|
# These attributes are provided by ``RequestBase``.
|
||||||
|
return RequestLimits(
|
||||||
|
max_content_length=super().max_content_length,
|
||||||
|
max_form_memory_size=super().max_form_memory_size or 500_000,
|
||||||
|
max_form_parts=super().max_form_parts or 1_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
return RequestLimits(
|
||||||
|
max_content_length=current_app.config["MAX_CONTENT_LENGTH"],
|
||||||
|
max_form_memory_size=current_app.config["MAX_FORM_MEMORY_SIZE"],
|
||||||
|
max_form_parts=current_app.config["MAX_FORM_PARTS"],
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_content_length(self) -> int | None:
|
def max_content_length(self) -> int | None:
|
||||||
"""The maximum number of bytes that will be read during this request. If
|
"""The maximum number of bytes that will be read during this request. If
|
||||||
|
|
@ -80,10 +127,8 @@ class Request(RequestBase):
|
||||||
if self._max_content_length is not None:
|
if self._max_content_length is not None:
|
||||||
return self._max_content_length
|
return self._max_content_length
|
||||||
|
|
||||||
if not current_app:
|
limits = self._get_limits()
|
||||||
return super().max_content_length
|
return limits.max_content_length
|
||||||
|
|
||||||
return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
@max_content_length.setter
|
@max_content_length.setter
|
||||||
def max_content_length(self, value: int | None) -> None:
|
def max_content_length(self, value: int | None) -> None:
|
||||||
|
|
@ -107,10 +152,8 @@ class Request(RequestBase):
|
||||||
if self._max_form_memory_size is not None:
|
if self._max_form_memory_size is not None:
|
||||||
return self._max_form_memory_size
|
return self._max_form_memory_size
|
||||||
|
|
||||||
if not current_app:
|
limits = self._get_limits()
|
||||||
return super().max_form_memory_size
|
return limits.max_form_memory_size
|
||||||
|
|
||||||
return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
@max_form_memory_size.setter
|
@max_form_memory_size.setter
|
||||||
def max_form_memory_size(self, value: int | None) -> None:
|
def max_form_memory_size(self, value: int | None) -> None:
|
||||||
|
|
@ -134,10 +177,8 @@ class Request(RequestBase):
|
||||||
if self._max_form_parts is not None:
|
if self._max_form_parts is not None:
|
||||||
return self._max_form_parts
|
return self._max_form_parts
|
||||||
|
|
||||||
if not current_app:
|
limits = self._get_limits()
|
||||||
return super().max_form_parts
|
return limits.max_form_parts
|
||||||
|
|
||||||
return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
@max_form_parts.setter
|
@max_form_parts.setter
|
||||||
def max_form_parts(self, value: int | None) -> None:
|
def max_form_parts(self, value: int | None) -> None:
|
||||||
|
|
@ -254,4 +295,4 @@ class Response(ResponseBase):
|
||||||
return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return]
|
return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return]
|
||||||
|
|
||||||
# return Werkzeug's default when not in an app context
|
# return Werkzeug's default when not in an app context
|
||||||
return super().max_cookie_size
|
return super().max_cookie_size
|
||||||
Loading…
Add table
Add a link
Reference in a new issue