Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.65% covered (success)
95.65%
22 / 23
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPayloadGuardMiddleware
95.45% covered (success)
95.45%
21 / 22
50.00% covered (danger)
50.00%
1 / 2
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
11
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Api\Middleware;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Shared\Infrastructure\Http\ApiResponseTrait;
12use Psr\Http\Message\ResponseFactoryInterface;
13use Psr\Http\Message\ResponseInterface;
14use Psr\Http\Message\ServerRequestInterface;
15use Psr\Http\Server\MiddlewareInterface;
16use Psr\Http\Server\RequestHandlerInterface;
17
18/**
19 * REST API Payload Guard Middleware (OWASP ASVS 13.2.3 / NIST SC-5).
20 *
21 * Enforces maximum request body size (413) and Content-Type: application/json (415)
22 * on state-modifying REST endpoints to protect against DoS and parser exploits.
23 *
24 * @package App\Core\Api\Middleware
25 */
26final readonly class ApiPayloadGuardMiddleware implements MiddlewareInterface
27{
28    use ApiResponseTrait;
29
30    /** @var int Default max payload size in bytes (2 MB). */
31    public const int DEFAULT_MAX_BODY_BYTES = 2097152;
32
33    /**
34     * ApiPayloadGuardMiddleware constructor.
35     *
36     * @param ResponseFactoryInterface $responseFactory PSR-17 response factory.
37     * @param int                      $maxBodyBytes    Maximum permitted payload size in bytes.
38     */
39    public function __construct(
40        private ResponseFactoryInterface $responseFactory,
41        private int $maxBodyBytes = self::DEFAULT_MAX_BODY_BYTES
42    ) {
43    }
44
45    /**
46     * Processes request and enforces payload size and media type constraints.
47     *
48     * @param ServerRequestInterface  $request Server request.
49     * @param RequestHandlerInterface $handler Request handler.
50     * @return ResponseInterface Handled response or 413/415 error response.
51     */
52    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
53    {
54        $contentLength = (int) $request->getHeaderLine('Content-Length');
55        $bodySize = $request->getBody()->getSize() ?? 0;
56        $contentType = strtolower($request->getHeaderLine('Content-Type'));
57        $isMultipart = str_contains($contentType, 'multipart/form-data');
58        $effectiveLimit = $isMultipart ? max($this->maxBodyBytes, 62914560) : $this->maxBodyBytes;
59
60        if ($contentLength > $effectiveLimit || $bodySize > $effectiveLimit) {
61            $msg = $isMultipart
62                ? 'Payload Too Large: Request body exceeds maximum allowed limit of 60MB.'
63                : 'Payload Too Large: Request body exceeds maximum allowed limit of 2MB.';
64            return $this->jsonError($this->responseFactory, $msg, 413);
65        }
66
67        $method = strtoupper($request->getMethod());
68        if (in_array($method, ['POST', 'PUT', 'PATCH'], true) && ($contentLength > 0 || $bodySize > 0)) {
69            $isJson = str_contains($contentType, 'application/json')
70                || str_contains($contentType, 'application/merge-patch+json');
71
72            if (!$isJson && !$isMultipart) {
73                return $this->jsonError(
74                    $this->responseFactory,
75                    'Unsupported Media Type: Request Content-Type must be application/json.',
76                    415
77                );
78            }
79        }
80
81        return $handler->handle($request);
82    }
83}