Skip to content
Merged
130 changes: 109 additions & 21 deletions ContactsEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,11 @@ class ContactsEncoder
*/
protected $global_tel_pattern;

/**
* @var array
* @psalm-suppress PossiblyUnusedProperty
*/
protected $aria_matches = array();

/**
* @var array Placeholder => original aria-label for restore
*/
protected $aria_placeholders = array();

/**
* @var int Counter for unique aria-label placeholders
*/
protected $aria_index = 0;

/**
* Attributes with possible email-like content to drop from the content to avoid unnecessary encoding.
* Key is a tag we want to find, value is an attribute with email to drop.
Expand Down Expand Up @@ -269,7 +258,9 @@ public function modifyContent($content, $skip_exclusions = false)
}

// modify content to prevent aria-label replaces by hiding it
$content = $this->handleAriaLabelContent($content);
if ( $this->do_encode_emails || $this->do_encode_phones ) {
$content = $this->handleAriaLabelContent($content);
}

Comment thread
svfcode marked this conversation as resolved.
// will use this in regexp callback
$this->temp_content = $content;
Expand All @@ -282,6 +273,10 @@ public function modifyContent($content, $skip_exclusions = false)

$this->do_encode_phones && $content = $this->modifyGlobalPhoneNumbers($content);

if ( $this->do_encode_emails || $this->do_encode_phones ) {
$content = $this->handleAriaLabelContent($content, true);
}

return $content;
}

Expand All @@ -293,6 +288,11 @@ public function modifyContent($content, $skip_exclusions = false)
*/
public function modifyGlobalEmails($content)
{
$owns_aria_protection = empty($this->aria_placeholders);
if ( $owns_aria_protection ) {
$content = $this->handleAriaLabelContent($content);
}

Comment thread
svfcode marked this conversation as resolved.
$replacing_result = preg_replace_callback($this->global_email_pattern, function ($matches) {
if ( isset($matches[3]) && in_array(strtolower($matches[3]), ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']) && isset($matches[0]) ) {
return $matches[0];
Expand Down Expand Up @@ -329,8 +329,9 @@ public function modifyGlobalEmails($content)
return '';
}, $content);

// modify content to turn back aria-label
$replacing_result = $this->handleAriaLabelContent($replacing_result, true);
if ( $owns_aria_protection ) {
$replacing_result = $this->handleAriaLabelContent($replacing_result, true);
}

//please keep this var (do not simplify the code) for further debug
return $replacing_result;
Expand All @@ -345,6 +346,11 @@ public function modifyGlobalEmails($content)
*/
public function modifyGlobalPhoneNumbers($content)
{
$owns_aria_protection = empty($this->aria_placeholders);
if ( $owns_aria_protection ) {
$content = $this->handleAriaLabelContent($content);
}

$phones_pattern = $this->global_phones_pattern;
$replacing_result = preg_replace_callback(
$phones_pattern,
Expand Down Expand Up @@ -385,8 +391,9 @@ function ($matches) {
$content
);

// modify content to turn back aria-label
$replacing_result = $this->handleAriaLabelContent($replacing_result, true);
if ( $owns_aria_protection ) {
$replacing_result = $this->handleAriaLabelContent($replacing_result, true);
}

//please keep this var (do not simplify the code) for further debug
return $replacing_result;
Expand Down Expand Up @@ -810,17 +817,17 @@ private static function dropAttributesContainEmail($content, $tags)
private function handleAriaLabelContent($content, $reverse = false)
{
if ( !$reverse ) {
$this->aria_matches = array();
$this->aria_placeholders = array();
$this->aria_index = 0;
if ( !$this->isSecureAriaLabelPlaceholderAvailable() ) {
return $content;
}
return preg_replace_callback($this->aria_regex, array($this, 'replaceAriaLabelWithPlaceholder'), $content);
}
if ( !empty($this->aria_placeholders) ) {
foreach ($this->aria_placeholders as $placeholder => $original) {
$content = str_replace($placeholder, $original, $content);
$content = $this->restoreAriaLabelPlaceholder($content, $placeholder, $original);
}
$this->aria_placeholders = array();
$this->aria_index = 0;
}
return $content;
}
Expand All @@ -836,8 +843,89 @@ private function replaceAriaLabelWithPlaceholder($matches)
return '';
}
$original = $matches[0];
$placeholder = 'ct_temp_aria_' . $this->aria_index++;
$placeholder = $this->generateAriaLabelPlaceholder();
if ( $placeholder === null ) {
return $original;
}
$this->aria_placeholders[$placeholder] = $original;
return $placeholder;
}

/**
* Whether a cryptographically secure placeholder can be generated.
*
* @return bool
*/
private function isSecureAriaLabelPlaceholderAvailable()
{
return function_exists('random_bytes') || function_exists('openssl_random_pseudo_bytes');
}

/**
* Build an unguessable placeholder so attacker-controlled content cannot collide with it.
*
* @return string|null Null when no secure entropy source is available.
*/
private function generateAriaLabelPlaceholder()
{
$bytes = $this->getSecureRandomBytes(16);
if ( !is_string($bytes) || strlen($bytes) !== 16 ) {
return null;
}

return '%%APBCT_ARIA_' . bin2hex($bytes) . '%%';
}

/**
* @param int $length
*
* @return string|null
*/
private function getSecureRandomBytes($length)
{
if ( !is_int($length) || $length < 1 ) {
return null;
}

if ( function_exists('random_bytes') ) {
try {
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_bytesFound
$bytes = random_bytes($length);
if ( strlen($bytes) === $length ) {
return $bytes;
}
} catch ( \Exception $e ) {
// Fall through to OpenSSL.
}
}

if ( function_exists('openssl_random_pseudo_bytes') ) {
$crypto_strong = false;
$bytes = openssl_random_pseudo_bytes($length, $crypto_strong);
if ( $crypto_strong && is_string($bytes) && strlen($bytes) === $length ) {
return $bytes;
}
}
Comment thread
Copilot marked this conversation as resolved.

return null;
}

/**
* Restore a single aria-label placeholder at its first occurrence only.
*
* @param string $content
* @param string $placeholder
* @param string $original
*
* @return string
*/
private function restoreAriaLabelPlaceholder($content, $placeholder, $original)
{
$pos = strpos($content, $placeholder);
if ( $pos === false ) {
return $content;
}

return substr($content, 0, $pos) . $original . substr($content, $pos + strlen($placeholder));
}
}
Loading
Loading