github actions|integration|mezzio-valinor|
Roave has been working on https://github.com/mezzio/mezzio-valinor, an integration layer that simplifies and improves security of HTTP input handling for PSR-15 applications that optionally rely on mezzio/mezzio-router.
Most HTTP applications can be roughly synthesised into following flow:
sequenceDiagram
Client -->> HTTP Layer: send request
critical what we discuss in this article
HTTP Layer -->> HTTP Layer: interpret request
end
HTTP Layer -->> Backend Logic: command
Backend Logic -->> HTTP Layer: result
HTTP Layer -->> Client: response
It turns out that “interpreting a request” is a non-trivial problem space, which often leads to vulnerabilities.
One such kind of vulnerability usually spawns from sharing the same data structure between input data, and interpreted information (such as in following example):
function handle(ServerRequestInterface $request): ResponseInterface
$data = $request->getParsedBody();
if (! is_array($data)) {
return DefaultResponses::malformedInput();
}
$sku = $data['sku'] ?? null;
$quantity = $data['quantity'] ?? null;
if (! (is_string($sku) && is_int($quantity) && $quantity > 0)) {
return DefaultResponses::validationError();
}
$this->cart->add($data);
return DefaultResponses::ok();
}
The above example is showing what one would do when no supporting abstraction is in place to validate request data.
This kind of code is repetitive, tedious and error-prone: also, it gets developers quite bored.
In addition to these pitfalls, the code above is vulnerable to security issues depending on how the internals of the used $this->cart->cart() method: we are passing the raw request body to an inner domain service!
What if our data includes a 'user_id' key? What if it includes a 'sudo' field? These may be innocuous now, but the cart service can vary behaviour over time!
The fact that we pass raw data to our service violates the “Parse, don’t validate” principle, which is a warmly recommended read.
A simple fix is to only extract the data that we want:
$this->cart->add(AddToCartCommand::extractFrom($data));
This works, and it will look like following:
final readonly class AddToCartCommand
{
public readonly string $sku;
public readonly int $quantity;
public static function extractFrom(array $data) {
assert(is_string($data['sku']));
assert('' !== $data['sku']);
assert(array_key_exists('quantity', $data));
assert(is_int($data['quantity']));
assert($data['quantity'] > 0);
return new self($data['sku'], $data['quantity']);
}
}
With this, we obtained that the cart will only ever be given the context necessary for performing the requested operation, without any space for future mis-interpretation, should the cart service change.
We achieved a small anticorruption layer.
Notice anything though? Our AddToCartCommand is going to explode with an AssertionError if any incorrect data is given!
In addition to ::extractFrom() being crash-happy, the final result also does not tell us whether the $sku looks valid, because we store a string, tending towards primitive obsession.
As we progress into our task of “interpreting” and giving “higher meaning” to our data, we will end with more domain-related details, and more rigid type information:
final readonly class AddToCartCommand
{
/** @var int<1, max> */
public readonly int $quantity;
public readonly Sku $sku;
public readonly ?DiscountCode $code;
private function __construct(/* ... */) {}
}
This kind of structure could be called a DTO, and it effectively helps us by reducing the size of our domain/codomain mapping, preventing any mis-interpretation of data that may fall out of it.
Beware that all we did here is enforcing structural validation of our data, by making invalid states impossible to represent.
We did not verify if the Sku exists: that is contextual validation, and it must still be applied, both in the presentation and domain layers, but more importantly, in the domain layer.
Mathias Verraes blogged about this validation separation in 2015.
At this point, we have certainly improved our understanding of the importance of data interpretation.
It is still very tedious to safely obtain AddToCartCommand payloads, handle all the edge cases, and to produce meaningful error messages whenever data is malformed, or missing.
There are a number of libraries out there, that attempt to parse raw data into well-formed structures.
A list of non-exhaustive examples:
laminas/laminas-form, which provides abstractions for “binding” data to a DTO, but doing so in a mutable manner: it does not really solve the problem of de-serialization.symfony/form, which is affected by similar problems to those of laminas/laminas-form.AddToCartCommand via a reflection de-serializer: a more complete solution, which sadly requires full buy-in into the platform, though.The realm of “mapping” is vast: you will find hundreds of libraries and tools, and this
article effectively adds one to the pile 🙂
We at Roave like working with cuyz/valinor.
Valinor is a surprisingly simple de-serialization library, which turns amorphous data into structured information:
$input = (new \CuyZ\Valinor\MapperBuilder())
// further refine the mapper here, if you need to
->mapper()
->map(AddToCartCommand::class, $request->parsedBody());
That’s all that’s needed to produce an AddToCartCommand: Valinor will do the heavy lifting for you.
It will:
mezzio-valinorOver the past month, Marco Pivetta, Romain Canon and George Steel have been closely working together on mezzio-valinor.
If you already use Mezzio, the usage is as follows:
1. Install the component (and add it to your config providers)
2. Wire a request handler like following:
final readonly class AddToCartHandler implements RequestHandlerInterface {
public function __construct(
private \Cuyz\Valinor\TreeMapper $mapper,
private ShoppingCart $cart,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$this->cart->add(
$this->mapper->map(AddToCartCommand::class, $request)
);
return DefaultResponses::ok();
}
}
3. Wire your middleware with your DIC
return [
'dependencies' => [
'factories' => [
// no need for custom code, reflection will do
AddToCartHandler::class => ReflectionBasedAbstractFactory::class,
],
],
];
As you can see, the amount of code to be written is minimal: the mapper will take care of extracting data from the PSR-7 ServerRequestInterface, it will map any inputs to their final shape, and it wil throw a MappingError, if anything doesn’t match expectations.
Behind the scenes, the entire mezzio-valinor package is:
ServerRequestInterfaceSo far, what this component achieved is massively reducing structural validation code within Mezzio applications.
We intend to further expand the component by:
MappingErrors intoThis new component is a good example of a complex problem space, with years of cumulative research and progress, solved with 4~5 actual lines of integration code, following lots of discussions and lots of integration tests.
Looking forward to y’all working with it!