Input Validation Strategies: Allowlisting Instead of Blocklisting
AI generated
OWASP
0x00
Security · OWASP · Input Validation · PHP
Input Validation Strategies: Allowlisting Instead of Blocklisting
Why allowlisting is the only robust line of defense

Anyone who checks user input only against known attack patterns inevitably misses the next variant. This article explains why allowlist validation beats every blocklist, how to correctly use PHP's filter_var function, which pitfalls Magento's form validation holds, and why validation and sanitization are strictly separate tasks with different goals.

14 min read Allowlist · Blocklist · filter_var PHP 8.4 · Magento 2.4.8

1. Why allowlists beat blocklists

A blocklist (denylist) enumerates what is forbidden: specific characters, specific words, specific patterns. The core problem is structural and cannot be fixed with more diligence: a denylist can only block attack patterns that were known at the time it was written. Every new bypass technique, every alternative encoding, every case trick, or every Unicode homoglyph that is not explicitly listed slips right through. A classic example: a filter blocks <script> but lets <img src=x onerror=alert(1)> pass, because nobody thought of that attribute.

An allowlist (whitelist) reverses the principle: it defines exactly what is permitted and implicitly rejects everything else. This is the fail closed principle instead of fail open. A product SKU field that only permits uppercase letters, digits, and hyphens cannot be defeated by any bypass technique, no matter how creative, because any input that does not exactly match the permitted pattern is discarded. The extra effort lies in the design: you need to know in advance which values are valid. That effort pays off, because unlike a denylist, an allowlist does not need constant maintenance against newly discovered attack techniques.

2. The right validation layer: server-side is mandatory

Client-side validation with HTML5 attributes like required, pattern, or maxlength, combined with supporting JavaScript, improves the user experience because errors become visible immediately, without a round trip to the server. It is, however, never a security boundary. Any request can be sent directly to the server via curl, through the browser DevTools, or with a tool like Burp Suite, bypassing every client-side check entirely. Relying on a form's pattern attribute to prevent SQL injection or XSS means, in effect, having no validation at all.

The rule is therefore unambiguous: every input is validated server-side, regardless of whether a client-side check also exists. Client-side validation is pure UX convenience and may exist redundantly alongside server-side validation, but never as a replacement for it. This applies even to input that appears to come from trusted sources, such as internal admin forms, REST endpoints protected by an API key, or CSV imports. Every boundary between systems, even within the same codebase between a controller and a service layer, is a potential entry point for unvalidated data, especially once services get reused elsewhere.

3. Type, format, range and length: the four checking dimensions

Robust validation does not just check whether a value "looks somewhat plausible"; it deliberately covers four dimensions. Type: a numeric field must actually be an integer or float, not a string that happens to look numeric. PHP's weak typing makes this especially important, since "0e123" == "0e456" evaluates to true under loose comparison logic. Format: a pattern like an email address, an IBAN, or a product SKU follows a fixed structure that should be checked with an anchored regex, meaning ^ at the start and $ at the end, so a partial substring match is never enough.

Range: numeric values need explicit upper and lower bounds, such as an order quantity between 1 and 999 or a discount percentage between 0 and 100. Without a range check, a form will happily accept a quantity of minus five or ten million. Length: strings need minimum and maximum limits, both from a business perspective, for example a company name capped at 255 characters to match the database column, and from a security perspective, to prevent denial of service through oversized payloads or ReDoS-prone regex evaluation. All four dimensions belong in every non-trivial validation routine.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Validator;

use InvalidArgumentException;

/**
 * Strict allowlist validator for URL slugs.
 * Rejects everything that is not explicitly permitted.
 */
final class SlugValidator
{
    // Only lowercase letters, digits and hyphens, 1-80 chars, anchored on both ends
    private const PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/';
    private const MAX_LENGTH = 80;

    /**
     * Validates a slug against the allowlist pattern and length bounds.
     *
     * @param string $slug Raw input value.
     * @return string The validated slug.
     * @throws InvalidArgumentException If the slug is not allowed.
     */
    public function validate(string $slug): string
    {
        if ($slug === '' || mb_strlen($slug) > self::MAX_LENGTH) {
            throw new InvalidArgumentException('Slug length out of allowed range.');
        }

        // preg_match returns 1 only on a full anchored match, never a partial one
        if (preg_match(self::PATTERN, $slug) !== 1) {
            throw new InvalidArgumentException(sprintf('Slug "%s" is not on the allowlist.', $slug));
        }

        return $slug;
    }
}

4. PHP filter_var(): filters, flags and pitfalls

PHP's built-in filter_var() function covers the most common validation cases without requiring an external library. FILTER_VALIDATE_INT, FILTER_VALIDATE_FLOAT, FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL, and FILTER_VALIDATE_IP return the (typed) value on valid input and false on invalid input. This is exactly where the most common pitfall lies: checking false with a loose comparison (==) instead of a strict comparison (===) causes bugs, because 0, a legitimate integer return value for a valid zero input, is also treated as "falsy" under loose logic.

Another pitfall: FILTER_VALIDATE_EMAIL only checks syntax against RFC 822 and related standards, never whether the domain exists or has an MX record. Real deliverability requires an additional DNS lookup or a double opt-in. The min_range and max_range options on FILTER_VALIDATE_INT allow range checking directly inside the filter call, without extra code. It is also important not to confuse FILTER_VALIDATE_* with FILTER_SANITIZE_*: sanitize filters silently transform input instead of rejecting invalid values, and are therefore no substitute for real validation.


<?php

declare(strict_types=1);

// Common filter_var() pitfalls and correct usage

// PITFALL: FILTER_VALIDATE_INT returns false on failure, but false == 0 in loose comparisons
$rawId = '0';
$productId = filter_var($rawId, FILTER_VALIDATE_INT);
if ($productId === false) {          // must use strict comparison
    throw new InvalidArgumentException('Invalid product id.');
}

// Bounded integer validation with min_range / max_range options
$page = filter_var($_GET['page'] ?? '1', FILTER_VALIDATE_INT, [
    'options' => ['default' => 1, 'min_range' => 1, 'max_range' => 500],
]);

// PITFALL: FILTER_VALIDATE_EMAIL accepts syntactically valid but non-deliverable addresses
// and does NOT verify the domain has an MX record - treat it as a syntax check only
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    throw new InvalidArgumentException('Invalid email format.');
}

// FILTER_VALIDATE_FLOAT with decimal option for locale-sensitive input
$price = filter_var('19,99', FILTER_VALIDATE_FLOAT, [
    'options' => ['decimal' => ','],
]);

// PITFALL: filter_var() with FILTER_SANITIZE_* silently transforms instead of rejecting
// Do not use sanitize filters where validation (rejection) is required
$unsafe = filter_var($_GET['comment'] ?? '', FILTER_SANITIZE_SPECIAL_CHARS); // wrong tool for validation

5. Validation libraries: Respect/Validation and Symfony Validator

For rules more complex than individual filter_var() calls, a dedicated validation library pays off. Respect/Validation offers a fluent, chainable API where each rule is composed as its own validator, for example v::stringType()->length(3, 32)->regex($pattern). The library throws a ValidationException on assert() with a list of every violated rule, giving precise feedback for API responses or form errors without having to write your own error-collection logic.

Symfony Validator follows a declarative approach based on constraint objects that can be used in any PHP project independently of the rest of the Symfony framework, including inside Magento modules. Constraints like Assert\NotBlank, Assert\Length, and Assert\Regex are combined into a list and checked against a value; the result is a ConstraintViolationList. Both libraries consistently follow the allowlist principle: you positively define what makes a value valid, instead of maintaining exclusion lists of invalid values. The advantage over hand-rolled validators lies in already-tested edge cases, such as Unicode normalization or multibyte length calculation.


<?php

declare(strict_types=1);

use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\ValidationException;

// Respect/Validation: declarative allowlist chain, collects all failed rules
$customerAgeValidator = v::intType()->between(18, 120);

try {
    $customerAgeValidator->assert($input['age']);
} catch (ValidationException $e) {
    // $e->getMessages() returns one message per failed rule
    throw new InvalidArgumentException(implode(' ', $e->getMessages()));
}

// Chained allowlist validator for a SKU field
$skuValidator = v::stringType()
    ->length(3, 32)
    ->regex('/^[A-Z0-9\-]+$/');

if (!$skuValidator->validate($input['sku'])) {
    throw new InvalidArgumentException('SKU does not match the allowed pattern.');
}

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;

// Symfony Validator: constraint chain equivalent to the SKU rule above
$validator = Validation::createValidator();
$violations = $validator->validate($input['sku'], [
    new Assert\NotBlank(),
    new Assert\Length(min: 3, max: 32),
    new Assert\Regex(pattern: '/^[A-Z0-9\-]+$/'),
]);

if (count($violations) > 0) {
    throw new InvalidArgumentException((string) $violations);
}

6. Magento form validation: DataObject and form XML

Magento defines validation rules for UI Component forms declaratively in form XML under <validation>, with rules like required-entry, validate-length, min_text_length, max_text_length, or validate-digits. These rules are evaluated client-side via Magento's jQuery Validation integration and provide immediate feedback in the admin grid or in storefront forms. What matters: these XML rules run in the browser and are exactly the kind of convenience validation that must never serve as the sole line of defense. Every controller, repository, and service that receives the submitted data must validate it again server-side.

Magento\Framework\DataObject itself does not validate automatically; it is a plain data container with magic getters and setters. Validation must be implemented explicitly in the save logic, in a repository, or in a dedicated validator service before data is written to the database. A common mistake is loading form data straight from getRequest()->getParams() into a DataObject and passing it, unchecked, to a repository that itself performs no validation either.


<?xml version="1.0"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <fieldset name="general">
        <field name="sku" formElement="input">
            <settings>
                <dataType>text</dataType>
                <label translate="true">SKU</label>
                <!-- Client-side rendered validation rules, mirrored by JS UI but never a substitute for server checks -->
                <validation>
                    <rule name="required-entry" xsi:type="boolean">true</rule>
                    <rule name="validate-length" xsi:type="boolean">true</rule>
                    <rule name="min_text_length" xsi:type="number">3</rule>
                    <rule name="max_text_length" xsi:type="number">32</rule>
                    <rule name="validate-code" xsi:type="boolean">true</rule>
                </validation>
            </settings>
        </field>
        <field name="qty" formElement="input">
            <settings>
                <dataType>number</dataType>
                <validation>
                    <rule name="required-entry" xsi:type="boolean">true</rule>
                    <rule name="validate-zero-or-greater" xsi:type="boolean">true</rule>
                    <rule name="validate-digits" xsi:type="boolean">true</rule>
                </validation>
            </settings>
        </field>
    </fieldset>
</form>

7. Magento\Framework\Validator and EAV attribute validation

Magento\Framework\Validator provides a ValidatorChain that combines several rules into a reusable, server-side check chain, such as Magento\Framework\Validator\StringLength for length checks or Magento\Framework\Validator\Regex for format checks. Unlike form XML rules, this chain runs in the server's PHP code and can therefore actually serve as a security boundary. A validator service that wraps the chain can be injected via dependency injection into repositories, save controllers, and import handlers, ensuring consistent rules regardless of the entry channel.

EAV attribute validation is configured declaratively via input_filter and validate_rules on the attribute, for example as alphanum-with-spaces or with a custom regex in the attribute's backend form. These rules, however, only apply when the storage path actually goes through the EAV attribute model, meaning through Magento\Eav\Model\Attribute and its associated backend models. Direct SQL writes, bulk imports through Magento\ImportExport with custom processing, or REST endpoints that bypass repositories, all skip this validation and require their own explicit checks.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Model;

use Magento\Framework\Validator\ValidatorChain;
use Magento\Framework\Validator\StringLength;
use Magento\Framework\Validator\Regex;
use Magento\Framework\Validator\Exception as ValidatorException;

/**
 * Server-side validation using Magento's Framework\Validator chain.
 * Runs independently of any EAV attribute validation configured in the admin.
 */
final class MetaTitleValidator
{
    /**
     * Builds and runs the allowlist validation chain for a meta title.
     *
     * @param string $value Raw meta title input.
     * @return void
     * @throws ValidatorException If any rule in the chain fails.
     */
    public function validate(string $value): void
    {
        $chain = new ValidatorChain();
        $chain->addValidator(new StringLength(['min' => 1, 'max' => 70]));
        // Anchored allowlist: letters, digits, spaces and a small set of punctuation only
        $chain->addValidator(new Regex(['pattern' => '/^[\p{L}\p{N}\s\-.,:!?]+$/u']));

        if (!$chain->isValid($value)) {
            throw new ValidatorException(implode(' ', $chain->getMessages()));
        }
    }
}

// EAV attribute validation is configured declaratively, not in PHP:
// bin/magento setup:upgrade after adding input_filter / validate_rules to an eav_attribute row,
// e.g. 'input_filter' => 'trim', 'validate_rules' => serialize(['input_validation' => 'alphanum-with-spaces'])

8. Canonicalization before validation

A validation rule checks a concrete byte sequence, not a "meaning". When the same logical input can be represented in multiple encodings, for example through URL encoding, double URL encoding, Unicode normalization forms (NFC/NFD), UTF-8 overlong encoding, or mixed-case file paths, a regex that covers one form may let another form through. This is exactly the mechanism behind many path traversal and filter bypass attacks: %2e%2e%2f or ..%c0%af slip past a filter that only searches for ../.

The consequence: canonicalization must happen before validation, not after. Input is first brought into a single, defined normal form, for example via urldecode() repeated until it stabilizes, Normalizer::normalize($str, Normalizer::FORM_C) for Unicode text, or realpath() for filesystem paths, before the allowlist check runs. If validation happens first and decoding happens afterward, a malicious payload can pass the check in its encoded form and only take on its actual, disallowed shape after decoding. This ordering, normalize before checking rather than the other way around, is one of the most frequently overlooked points in validation routines.

9. Validation vs. sanitization: two different jobs

Validation and sanitization are often used interchangeably in practice, but they are fundamentally different operations with different guarantees. Validation is a binary decision: a value either matches the allowlist or it is rejected. There is no "partially valid". Sanitization, on the other hand, transforms an input, for example by removing, escaping, or replacing certain characters, and always returns a result, regardless of what the original input looked like.

The potential for conflict arises when the two concepts get mixed up: a developer "validates" an input by stripping dangerous characters with strip_tags() or htmlspecialchars(), and then wrongly assumes the input is now checked and safe for every context. Sanitization for the HTML output context does not automatically make an input valid for a SQL query, a file path, or a business rule like a positive order quantity. The clean separation is this: validation decides whether an input is accepted from a business and structural standpoint, while context-specific escaping or encoding (sanitization in the narrow sense) happens separately, right before the value is used in its specific output context, and never as a replacement for the upstream validation.

Task Insecure / Naive Recommended strategy Benefit
Email checking Manually maintain a blocklist of forbidden characters filter_var($v, FILTER_VALIDATE_EMAIL) Tested, allowlist-based format
Special characters in free text str_replace() of known dangerous strings Permitted character set via anchored regex No arms race against new bypasses
Validation layer Client-side HTML5/JS pattern only Mandatory server-side check, client is UX only Cannot be bypassed via curl
Type checking Loose comparisons (==, is_numeric) Strict typing + FILTER_VALIDATE_INT No type-juggling traps
File upload Extension blocklist (.php, .phtml) Allowlist of permitted MIME types + content check No overlooked executable extensions

Mironsoft

Security audits, code reviews, and secure Magento development

Ready to lock down your input validation?

We audit your Magento and PHP codebase for blocklist pitfalls, missing server-side validation, and unclean separation between validation and sanitization, then implement robust allowlist strategies for forms, APIs, and import interfaces.

Security code review

Systematic review of every input point for allowlist compliance and OWASP standards

Validator refactoring

Replacing blocklist logic with Respect/Validation, Symfony Validator, or Framework\Validator

Form & API hardening

Securing Magento form XML, EAV attributes, and REST/GraphQL endpoints against bypasses

10. Summary

The most important takeaway of these input validation strategies fits in one sentence: an allowlist defines what is permitted and implicitly rejects everything else, while a blocklist only ever captures known attack patterns and inevitably has gaps. Server-side validation is non-negotiable, client-side checking is pure UX convenience. filter_var() covers many standard cases but demands strict comparisons and a clear understanding of what each filter actually checks. Validation libraries like Respect/Validation and Symfony Validator make complex rule chains readable and testable.

In Magento, form XML rules only run in the browser, while Magento\Framework\Validator and EAV attribute validation run server-side, but only on the paths that actually go through the attribute model. Canonicalization before checking prevents encoded payloads from slipping past an allowlist. And the clean separation between validation, which rejects, and sanitization, which transforms, prevents the dangerous assumption that an input sanitized for HTML output is automatically safe for every other context.

Input Validation Strategies: Allowlisting Instead of Blocklisting, the Essentials at a Glance

Allowlist over blocklist

Accept only explicitly permitted values, reject everything else by default. No arms race against new bypass techniques.

Server-side is mandatory

Client validation is UX convenience only, never a security boundary. Any request can bypass it.

Use filter_var() correctly

Strict comparisons, bounds options, and never use sanitize filters as a substitute for validation.

Validation vs. sanitization

Validation rejects, sanitization transforms. Keep them cleanly separated and never confuse the two.

11. FAQ: Input Validation Strategies

1What is the difference between an allowlist and a blocklist?
A blocklist enumerates forbidden patterns and inevitably misses new bypass techniques. An allowlist defines exactly what is permitted and implicitly rejects everything else.
2Why is client-side JavaScript validation not enough?
Any request can be sent via curl or Burp Suite without a browser, bypassing every client-side check. Server-side validation is therefore always mandatory.
3What is the biggest pitfall with filter_var()?
The false return value must be checked strictly with ===, otherwise a valid zero value gets treated as an error. FILTER_VALIDATE_EMAIL also only checks syntax, not deliverability.
4Validation library or a custom validator?
For single checks, filter_var() is enough. For complex rule chains, Respect/Validation or Symfony Validator make sense since they already cover tested edge cases.
5How does Magento validate forms server-side?
Form XML rules only run in the browser. Server-side you need explicit validation in the controller, repository, or via Magento\Framework\Validator.
6What is Magento\Framework\Validator?
A ValidatorChain that combines several server-side rules like StringLength or Regex. Fits repositories, save controllers, and import handlers.
7What does canonicalization before validation mean?
Input is brought into a single normal form before checking, for example via full URL decoding. Without this step, an encoded payload can bypass an allowlist.
8Validation vs. sanitization: what is the difference?
Validation accepts or rejects. Sanitization transforms and always returns a result. Input sanitized for HTML is not automatically safe in other contexts.
9How do you set up length and range checks correctly?
Strings need minimum and maximum limits to guard against oversized payloads. Numeric values need explicit upper and lower bounds, for example via min_range/max_range.
10Is EAV attribute validation sufficient for security?
Only partially. It only applies when the storage path goes through the attribute model. Direct SQL writes or custom import logic need additional validation.