RequestHandler

What is RequestHandler?

RH (alias for RequestHandler) provides secure, centralized access to HTTP request data. It automatically sanitizes input using vtlib_purify() and applies type-safe filters from F:: enum. This replaces direct access to superglobals like $_REQUEST, $_GET, $_POST, etc.
Our goal with this system is to prevent vulnerabilities ex. SQL Injection, XSS, File Inclusion, ecc.

How filtering works

RequestHandler applies a two-level filtering process:

// ex. index.php?module=Accounts&action=CustomAction&custom=123&another=TEST

// In config: 'module' => F::mod
$module = RH::r('module');               // Uses F::mod filter from config
$customParam = RH::r('custom', F::int);  // Not in config: vtlib_purify() + F::int
$anotherParam = RH::r('another');        // Not in config: only vtlib_purify()

This approach ensures:


DO

// ✅ Always access superglobals through RH
$module = RH::r('module', F::mod);
$id = RH::r('id', F::int);

// ✅ Always specify appropriate filters
$email = RH::r('email', F::email);
$age = RH::r('age', F::int);
$status = RH::r('status', F::enum(['active', 'inactive']));

// ✅ Check for null values when data is required
$userId = RH::r('user_id', F::int);
if ($userId === null) {
    throw new InvalidArgumentException('User ID is required');
}

// ✅ Use POST for operations that are sensitive or that modify the CRM state (e.g.: sending an email, saving a record, ...)
RH::ensureMethod('POST');

DON'Ts

Avoid these practices: They bypass security, are redundant and can introduce vulnerabilities.

// ❌ Direct superglobal access
$userId = $_REQUEST['user_id'];
$email = $_POST['email'];
$page = intval($_GET['page']);

// ❌ Manual sanitization
$name = vtlib_purify($_REQUEST['name']);
$description = vtlib_purify($_POST['description']);

// ❌ No filter applied
$age = RH::r('age');  // Returns string "25" instead of int 25
if ($age > 18) {      // String comparison may not work as expected
    // ...
}

// ❌ Using raw without justification
$data = RH::r_raw('data');  // Bypasses sanitization
echo $data;                 // Potential XSS vulnerability

// ❌ No null check for required data
$recordId = RH::r('record', F::int);
$record = getRecord($recordId);  // May fail if $recordId is null

// ❌ Accepting any HTTP method for sensitive operation or action that modify the CRM state
// No method check here - vulnerable to GET-based attacks
$settings = RH::r('settings', F::json);
saveSettings($settings);

// ❌ Type assumption without validation
$ids = RH::r('ids');     // Assuming it's an array
foreach ($ids as $id) {  // Fatal error if $ids is not an array
    // ...
}

Main methods

Method Description
RH::r($key, $filter) Access $_REQUEST with filter (optional)
RH::g($key, $filter) Access $_GET with filter (optional)
RH::p($key, $filter) Access $_POST with filter (optional)
RH::c($key, $filter) Access $_COOKIE with filter (optional)
RH::s($key, $filter) Access $_SERVER with filter (optional)
RH::f($key) Access uploaded files (PSR-7)
RH::r_mod() Module name from $_REQUEST
RH::r_action() Action name from $_REQUEST
RH::r_record() Record ID from $_REQUEST
RH::r_has($key) Verify key existence (sanitized)
RH::r_has_raw($key) Verify key existence (raw)
RH::r_all() All parameters sanitized
RH::r_all_raw() All parameters raw (unsanitized)
RH::ensureMethod($method) Force HTTP method (405 if different)
RH::isFromMobile() Check if request is from mobile app
RH::isFromPortal() Check if request is from portal

Basic filters

Filter Use case Valid input → Output Invalid input → Output
F::int IDs, numbers "42"42 "abc"0
nullnull
F::float Decimal numbers "3.14"3.14 "abc"0.0
nullnull
F::str General strings (no HTML; default max length: 1000 characters) "Hello""Hello"

"<b>abc</b>""abc" (tags stripped)

"Very long text..." → Truncated to 1000 chars by default
nullnull

F::str(max: 50) Limited strings (no HTML) "Short text""Short text" "Very long text..." → Truncated to 50 chars
nullnull
F::substr(max: 50) Substring (keeps HTML, may result in invalid HTML) "<b>text</b>""<b>text</b>" Long string → Truncated (HTML preserved)
nullnull
F::html HTML strings (purified) "<b>bold</b>""<b>bold</b>" "<script>alert(0)</script>""" (XSS removed)
nullnull
F::htmlurl HTML strings (purified, allow URLs) [only from vtenext >= 26.07] "<img src='...'>""<img src='...'>"
F::bool Boolean flags "1", "true", "yes"true "0", "false", "no"false
nullnull
F::email Email addresses "user@example.com""user@example.com" "invalid-email"null
"user@"null
nullnull
F::url URLs "https://example.com""https://example.com" "invalid-url"null
"htp://exa"null
nullnull
F::ip IP addresses "192.168.1.1""192.168.1.1" "999.999.999.999"null
"invalid-ip"null
nullnull
F::json JSON data '{"key":"value"}'["key" => "value"] 'invalid json'null
'{broken'null
nullnull

Specific filters

Filter Use case Valid input → Output Invalid input → Output
F::mod Module names "Accounts" → unchanged "Acc<script>""Accscript" (special chars removed)
nullnull
F::action Action names "Save", "Detail.View" → unchanged "Act!on@""Acton" (special chars removed)
nullnull
F::field Field names "firstname" → unchanged "field-name!""fieldname" (special chars removed)
nullnull
F::recfields Record/field IDs "123@-456", "1x2,3|4" → unchanged "123abc!@#""123@" (invalid chars removed)
nullnull
F::path File paths (dots allowed; use checkFileAccess to ensure secure file access) "modules/Accounts" → unchanged "path@with#special$chars""pathwithspecialchars" (keeps valid chars)
nullnull
F::subpath Sub paths (no dots; use checkFileAccess to ensure secure file access) "modules/Accounts" → unchanged "subpath.with.dots""subpathwithdots" (dots removed)
nullnull
F::alpha Alphabetic only "abcABC_" → unchanged "abc123!@#""abc" (numbers/symbols removed)
nullnull
F::alphanum Alphanumeric only "abcABC123_" → unchanged "abc123!@#""abc123" (symbols removed)
nullnull
F::enum([...]) Whitelist values "active" (if in list) → unchanged "invalid" (not in list) → null
nullnull
F::regex('/pattern/') Pattern matching "ABC123" (matches) → unchanged "invalid" (no match) → null
nullnull
F::datetime('Y-m-d') Date/time parsing "2026-01-08" → unchanged "invalid-date"null
"2026-99-99"null
nullnull
F::sep(',', 'intval') Split and transform "1,2,3"[1, 2, 3] Non-string → null
nullnull
F::jsfunc Safe JS functions "closePopup" (if whitelisted) → unchanged "evilFunction" (not whitelisted) → null
nullnull

Advanced filters

// Enum filter - restrict to specific values
$status = RH::r('status', F::enum(['active', 'inactive', 'pending']));

// Regex filter - custom pattern validation
$code = RH::r('code', F::regex('/^[A-Z]{3}\d{3}$/'));

// DateTime filter - parse date strings
$date = RH::r('date', F::datetime('Y-m-d'));

// Separator filter - split strings into arrays
$ids = RH::r('ids', F::sep(',', 'intval')); // "1,2,3" -> [1,2,3]

// Array filter - validate nested arrays
$user = RH::r('user', F::array([
    'name' => F::str(max: 100),
    'email' => F::email,
    'age' => F::int,
]));

// Custom callable filter
$customCode = RH::r('code', function($value) {
    if (!is_string($value)) return null;
    $value = strtoupper(trim($value));
    return preg_match('/^[A-Z0-9]{6}$/', $value) ? $value : null;
});

Examples

Simple form data
$name = RH::p('name', F::str(max: 100));
$email = RH::p('email', F::email);
$age = RH::p('age', F::int);

if ($name === null || $email === null) {
    throw new InvalidArgumentException('Name and email are required');
}
List page with pagination
$page = RH::g('page', F::int) ?? 1;
$limit = RH::g('limit', F::int) ?? 20;
$sort = RH::g('sort', F::enum(['name', 'date', 'id'])) ?? 'name';
$direction = RH::g('dir', F::enum(['asc', 'desc'])) ?? 'asc';
$query = RH::g('q', F::str(max: 255));
Complex form with nested arrays
$userData = RH::p('user', F::array([
    'personal' => F::array([
        'firstname' => F::str(max: 50),
        'lastname' => F::str(max: 50),
        'email' => F::email,
        'phone' => F::str(max: 20),
    ]),
    'preferences' => F::array([
        'language' => F::enum(['en', 'it', 'es']),
        'timezone' => F::str(max: 50),
    ]),
    'addresses' => F::array([
        'shipping' => F::array([
            'street' => F::str(max: 255),
            'city' => F::str(max: 100),
            'country' => F::str(max: 2),
        ]),
    ]),
]));
File upload
$uploadedFile = RH::f('document');

if ($uploadedFile && $uploadedFile->getError() === UPLOAD_ERR_OK) {
    $filename = $uploadedFile->getClientFilename();
    $mimeType = $uploadedFile->getClientMediaType();
    $size = $uploadedFile->getSize();
    
    // ...
} else {
    // Handle upload error
    $error = $uploadedFile ? $uploadedFile->getError() : 'No file uploaded';
}

Advanced Usage

Temporary request context

Push/pop operations should be used only for testing or very specific scenarios.

// Save current state and temporarily override
RH::push_r(['module' => 'TestModule', 'action' => 'TestAction']);

// Do something with temporary data
$module = RH::r_mod(); // Returns 'TestModule'

// Restore original state
RH::pop_r();

Configuration

The RequestHandler uses a configuration file to control request sanitization behavior and define safe parameters. Configuration is defined in config/request.config.php and can be extended via config/request.config.override.php.

preserve_original_request
'preserve_original_request' => false,  // default

Controls whether all original request parameters are preserved in the $_REQUEST superglobal:

Security note: When preserve_original_request is false, the $_REQUEST array is cleaned to contain only whitelisted keys. This prevents injection of unexpected parameters but does NOT bypass RH's access control - you should still use RH methods for all access.

safe_keys
'safe_keys' => [
    'module' => F::mod,
    'action' => F::action,
    'record' => F::int,
    'id' => F::int,
    'page' => F::int,
    'limit' => F::int,
    // ... add commonly used keys here
],

Defines a whitelist of parameter keys that are:

safe_js_functions
'safe_js_functions' => [
    'closePopup',
    'LPOP.create',
    'parent.ActionTaskScript.addStaticRelatedRecord',
    // ... other trusted JavaScript function names
],

Lists JavaScript function names that are allowed to be passed as request parameters (e.g., for callbacks). This prevents XSS attacks by restricting which functions can be executed client-side.

Creating custom configuration

To add your own safe keys without modifying the core configuration, create config/request.config.override.php:

// config/request.config.override.php
return [
    'safe_keys' => [
        // Your custom keys
        'custom_param' => F::str(max: 100),
        'my_record_id' => F::int,
    ],
    'safe_js_functions' => [
        // Your custom callback functions
        'MyApp.customCallback',
    ],
];

The override configuration is automatically merged with the default configuration.

Migration to version 26.04
During the upgrade to version 26.04, the system automatically scans for direct $_REQUEST superglobal usage. Any detected parameters are automatically added to config/request.config.override.php with the F::html filter to maintain backward compatibility and prevent breaking existing customizations. 

All core code has been refactored to use RH methods exclusively instead of direct superglobal access.


Revision #7
Created 2026-01-23 15:30:21 UTC by m.maporti
Updated 2026-07-18 07:55:27 UTC by Daniele