Input Sanitation

Canvas sanitization cleans and normalizes incoming request data before it reaches your controllers. The SanitizeAspect intercepts requests, applies transformation rules to the input data, and stores sanitized copies in the request attributes — leaving the original POST and GET data untouched.

How SanitizeAspect Works

When a request hits a route with @InterceptWith(SanitizeAspect::class):

  1. The aspect instantiates your sanitization class
  2. Retrieves sanitization rules by calling getRules()
  3. Sanitizes POST and GET data independently
  4. Builds a deterministic merged sanitized view
  5. Stores sanitized data in $request->attributes['sanitized']
  6. Continues to your controller with the modified request

Important: Sanitization does not overwrite $request->request or $request->query. The original input remains unchanged for debugging and traceability. Sanitized values are available in a dedicated attribute structure.

The stored structure looks like:

sanitized:
  post   → sanitized POST data
  query  → sanitized GET data
  merged → combined sanitized view (POST overrides GET on conflicts)

Basic Usage

Apply sanitization using the @InterceptWith annotation:

<?php
namespace App\Controllers;

use Quellabs\Canvas\Annotations\Route;
use Quellabs\Canvas\Annotations\InterceptWith;
use Quellabs\Canvas\Sanitization\SanitizeAspect;
use App\Sanitization\UserSanitization;
use Symfony\Component\HttpFoundation\Request;

class UserController extends BaseController {

    /**
     * @Route("/users/create", methods={"POST"})
     * @InterceptWith(SanitizeAspect::class, sanitizer=UserSanitization::class)
     */
    public function create(Request $request) {
        // Retrieve sanitized data
        $sanitized = $request->attributes->get('sanitized', []);
        $input = $sanitized['merged'] ?? [];

        $user = new User();
        $user->setName($input['name'] ?? null);
        $user->setEmail($input['email'] ?? null);
        $user->setBio($input['bio'] ?? null);

        $this->em()->persist($user);
        $this->em()->flush();

        return $this->redirect('/users');
    }
}

You may also access sanitized sources explicitly:

$post  = $sanitized['post'];
$query = $sanitized['query'];

Access Patterns

Sanitized data is intentionally separated from raw input to avoid accidental mutation and to make intent explicit.

  • Use merged when your controller treats input as a single payload
  • Use post or query when source distinction matters

The merged view applies deterministic precedence — POST values override GET values when keys collide.

Creating Sanitization Classes

Define transformation rules by implementing SanitizationInterface:

<?php
namespace App\Sanitization;

use Quellabs\Canvas\Sanitization\Contracts\SanitizationInterface;
use Quellabs\Canvas\Sanitization\Rules\Trim;
use Quellabs\Canvas\Sanitization\Rules\EmailSafe;
use Quellabs\Canvas\Sanitization\Rules\StripTags;
use Quellabs\Canvas\Sanitization\Rules\UrlSafe;

class UserSanitization implements SanitizationInterface {

    public function getRules(): array {
        return [
            'name' => [new Trim(), new StripTags()],
            'email' => [new Trim(), new EmailSafe()],
            'website' => [new Trim(), new UrlSafe()],

            // StripTags accepts array of allowed tag names (without angle brackets)
            'bio' => [new Trim(), new StripTags(['p', 'br', 'strong', 'em'])]
        ];
    }
}

Built-in Sanitization Rules

Text Cleaning

  • Trim - Remove leading/trailing whitespace
  • StripTags(array $allowed = []) - Remove HTML tags, optionally allow specific tag names like ['p', 'br', 'strong']
  • RemoveControlChars - Remove ASCII control characters (0x00-0x1F except tab/newline)
  • RemoveZeroWidth - Remove invisible Unicode characters (zero-width spaces, joiners, etc.)
  • NormalizeLineEndings - Convert all line endings to \n
  • RemoveStyleAttributes - Strip inline style attributes from HTML

Security Transformations

  • ScriptSafe - Remove script tags, event handlers, and javascript: URLs
  • SqlSafe - Remove common SQL metacharacters (not a substitute for parameterized queries)
  • RemoveNullBytes - Remove null bytes that can bypass security checks

Format Normalization

  • EmailSafe - Keep only valid email characters (letters, digits, @, ., -, +, _)
  • UrlSafe - Keep only valid URL characters
  • PathSafe - Remove directory traversal sequences (../, ..\, etc.)

Rule Chaining and Execution Order

Multiple rules execute left-to-right, with each rule receiving the output of the previous rule:

'description' => [
    new Trim(),                                    // 1. "  <p>Hello</p>  " → "<p>Hello</p>"
    new StripTags(['p', 'br', 'strong']),        // 2. "<p>Hello</p>" → "<p>Hello</p>"
    new RemoveStyleAttributes(),                   // 3. Removes any style="" attributes
    new NormalizeLineEndings()                    // 4. Converts \r\n to \n
]

// Input:  "  <p style='color:red'>Hello</p>\r\n  "
// Output: "<p>Hello</p>\n"

Creating Custom Sanitization Rules

Implement SanitizationRuleInterface to create custom transformations:

<?php
namespace App\Sanitization\Rules;

use Quellabs\Canvas\Sanitization\Contracts\SanitizationRuleInterface;

class CleanPhoneNumber implements SanitizationRuleInterface {

    public function sanitize(mixed $value): mixed {
        // Return non-strings unchanged
        if (!is_string($value)) {
            return $value;
        }

        // Remove all non-digit characters except leading +
        $cleaned = preg_replace('/[^0-9+]/', '', $value);

        // Ensure only one + at the start
        return str_starts_with($cleaned, '+')
            ? '+' . str_replace('+', '', $cleaned)
            : $cleaned;
    }
}

// Usage
'phone' => [new Trim(), new CleanPhoneNumber()]

// Input:  "  +1 (555) 123-4567  "
// Output: "+15551234567"

Security Considerations

Defense in Depth

Sanitization is one layer of security, not the complete solution:

  • Input Sanitization - Clean and normalize data (this feature)
  • Input Validation - Verify data meets business requirements
  • Parameterized Queries - Prevent SQL injection (sanitization alone is insufficient)
  • Output Escaping - Escape data when rendering in templates
  • CSP Headers - Browser-level XSS protection

Best Practices

  • Sanitize early: Sanitized data is available before controller logic while original input remains preserved
  • Be explicit: List all fields that need sanitization - unlisted fields pass through unchanged
  • Order matters: Place Trim first, format-specific rules last
  • Test edge cases: Verify behavior with empty strings, null values, and malformed input
  • Don't trust sanitization alone: Use parameterized queries, output escaping, and validation
  • Document allowed tags: When using StripTags with an allowlist, document why those tags are safe