Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/optimole-wp/inc/admin.php
+++ b/optimole-wp/inc/admin.php
@@ -131,11 +131,33 @@
if ( headers_sent() ) {
return;
}
- $policy = 'ch-viewport-width=(self "%1$s")';
+ $policy = $this->get_permissions_policy();
+ if ( empty( $policy ) ) {
+ return;
+ }
+ header( sprintf( 'Permissions-Policy: %s', $policy ), false );
+ }
+
+ /**
+ * Build the Permissions-Policy header value based on active settings.
+ *
+ * @return string Comma-separated policy directives, or empty string when none apply.
+ */
+ public function get_permissions_policy(): string {
+ $parts = [];
+ $service_url = esc_url( Optml_Config::$service_url );
+
+ if ( $this->settings->is_scale_enabled() ) {
+ $parts[] = sprintf( 'ch-viewport-width=(self "%s")', $service_url );
+ }
if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {
- $policy .= ', ch-ect=(self "%1$s")';
+ $parts[] = sprintf( 'ch-ect=(self "%s")', $service_url );
+ }
+ if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
+ $parts[] = sprintf( 'ch-dpr=(self "%s")', $service_url );
}
- header( sprintf( 'Permissions-Policy: %s', sprintf( $policy, esc_url( Optml_Config::$service_url ) ) ), false );
+
+ return implode( ', ', $parts );
}
/**
* Function that purges the image cache for a specific file.
@@ -1071,14 +1093,38 @@
return;
}
- $hints = 'Viewport-Width';
- if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {
- $hints .= ', ECT';
+ $hints = $this->get_accept_ch_hints();
+ if ( empty( $hints ) ) {
+ return;
}
echo sprintf( '<meta http-equiv="Accept-CH" content="%s" />', esc_attr( $hints ) );
}
/**
+ * Build the Accept-CH meta content value based on active settings.
+ *
+ * Mirrors the directives used in get_permissions_policy() so both
+ * the Permissions-Policy header and the Accept-CH meta stay in sync.
+ *
+ * @return string Comma-separated hint tokens, or empty string when none apply.
+ */
+ public function get_accept_ch_hints(): string {
+ $hints = [];
+
+ if ( $this->settings->is_scale_enabled() ) {
+ $hints[] = 'Viewport-Width';
+ }
+ if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {
+ $hints[] = 'ECT';
+ }
+ if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
+ $hints[] = 'DPR';
+ }
+
+ return implode( ', ', $hints );
+ }
+
+ /**
* Update daily the quota routine.
*/
public function daily_sync() {
--- a/optimole-wp/inc/settings.php
+++ b/optimole-wp/inc/settings.php
@@ -59,7 +59,7 @@
'cdn' => 'disabled',
'admin_bar_item' => 'enabled',
'lazyload' => 'disabled',
- 'scale' => 'disabled',
+ 'scale' => 'disabled', // Due to legacy reasons the disabled state means that the scale is enabled and the enabled state means that the scale is disabled.
'network_optimization' => 'enabled',
'lazyload_placeholder' => 'enabled',
'bg_replacer' => 'enabled',
@@ -662,6 +662,14 @@
public function is_best_format() {
return $this->get( 'best_format' ) === 'enabled';
}
+ /**
+ * Check if scale is enabled.
+ *
+ * @return bool Scale enabled
+ */
+ public function is_scale_enabled() {
+ return $this->get( 'scale' ) === 'disabled'; // Due to legacy reasons the disabled state means that the scale is enabled and the enabled state means that the scale is disabled.
+ }
/**
* Check if offload limit was reached.
--- a/optimole-wp/inc/tag_replacer.php
+++ b/optimole-wp/inc/tag_replacer.php
@@ -504,7 +504,11 @@
$optimized_url = $this->change_url_for_size( $new_url, $width, $height, $dpr );
if ( $optimized_url ) {
- $new_srcset_entries[] = $optimized_url . ' ' . $descriptor;
+ $escaped_url = esc_url( $optimized_url );
+ if ( empty( $escaped_url ) ) {
+ continue;
+ }
+ $new_srcset_entries[] = $escaped_url . ' ' . esc_attr( $descriptor );
// Add sizes attribute entry for responsive breakpoints
if ( $breakpoint > 0 ) {
--- a/optimole-wp/optimole-wp.php
+++ b/optimole-wp/optimole-wp.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Image optimization service by Optimole
* Description: Complete handling of your website images.
- * Version: 4.2.2
+ * Version: 4.2.3
* Author: Optimole
* Author URI: https://optimole.com
* License: GPL-2.0+
@@ -87,7 +87,7 @@
}
define( 'OPTML_URL', plugin_dir_url( __FILE__ ) );
define( 'OPTML_PATH', plugin_dir_path( __FILE__ ) );
- define( 'OPTML_VERSION', '4.2.2' );
+ define( 'OPTML_VERSION', '4.2.3' );
define( 'OPTML_NAMESPACE', 'optml' );
define( 'OPTML_BASEFILE', __FILE__ );
define( 'OPTML_PRODUCT_SLUG', basename( OPTML_PATH ) );
--- a/optimole-wp/vendor/autoload.php
+++ b/optimole-wp/vendor/autoload.php
@@ -19,4 +19,4 @@
require_once __DIR__ . '/composer/autoload_real.php';
-return ComposerAutoloaderInit6eb5b1cc07f2614058eaa1c6a8181fac::getLoader();
+return ComposerAutoloaderInit982a4078faab475dd9f9f90a3f065675::getLoader();
--- a/optimole-wp/vendor/composer/autoload_real.php
+++ b/optimole-wp/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
-class ComposerAutoloaderInit6eb5b1cc07f2614058eaa1c6a8181fac
+class ComposerAutoloaderInit982a4078faab475dd9f9f90a3f065675
{
private static $loader;
@@ -24,16 +24,16 @@
require __DIR__ . '/platform_check.php';
- spl_autoload_register(array('ComposerAutoloaderInit6eb5b1cc07f2614058eaa1c6a8181fac', 'loadClassLoader'), true, true);
+ spl_autoload_register(array('ComposerAutoloaderInit982a4078faab475dd9f9f90a3f065675', 'loadClassLoader'), true, true);
self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
- spl_autoload_unregister(array('ComposerAutoloaderInit6eb5b1cc07f2614058eaa1c6a8181fac', 'loadClassLoader'));
+ spl_autoload_unregister(array('ComposerAutoloaderInit982a4078faab475dd9f9f90a3f065675', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
- call_user_func(ComposerAutoloadComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac::getInitializer($loader));
+ call_user_func(ComposerAutoloadComposerStaticInit982a4078faab475dd9f9f90a3f065675::getInitializer($loader));
$loader->register(true);
- $filesToLoad = ComposerAutoloadComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac::$files;
+ $filesToLoad = ComposerAutoloadComposerStaticInit982a4078faab475dd9f9f90a3f065675::$files;
$requireFile = Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
--- a/optimole-wp/vendor/composer/autoload_static.php
+++ b/optimole-wp/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@
namespace ComposerAutoload;
-class ComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac
+class ComposerStaticInit982a4078faab475dd9f9f90a3f065675
{
public static $files = array (
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
@@ -116,9 +116,9 @@
public static function getInitializer(ClassLoader $loader)
{
return Closure::bind(function () use ($loader) {
- $loader->prefixLengthsPsr4 = ComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac::$prefixLengthsPsr4;
- $loader->prefixDirsPsr4 = ComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac::$prefixDirsPsr4;
- $loader->classMap = ComposerStaticInit6eb5b1cc07f2614058eaa1c6a8181fac::$classMap;
+ $loader->prefixLengthsPsr4 = ComposerStaticInit982a4078faab475dd9f9f90a3f065675::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit982a4078faab475dd9f9f90a3f065675::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInit982a4078faab475dd9f9f90a3f065675::$classMap;
}, null, ClassLoader::class);
}
--- a/optimole-wp/vendor/composer/installed.php
+++ b/optimole-wp/vendor/composer/installed.php
@@ -38,9 +38,9 @@
'dev_requirement' => false,
),
'enshrined/svg-sanitize' => array(
- 'pretty_version' => '0.21.0',
- 'version' => '0.21.0.0',
- 'reference' => '5e477468fac5c5ce933dce53af3e8e4e58dcccc9',
+ 'pretty_version' => '0.22.0',
+ 'version' => '0.22.0.0',
+ 'reference' => '0afa95ea74be155a7bcd6c6fb60c276c39984500',
'type' => 'library',
'install_path' => __DIR__ . '/../enshrined/svg-sanitize',
'aliases' => array(),
--- a/optimole-wp/vendor/enshrined/svg-sanitize/src/Sanitizer.php
+++ b/optimole-wp/vendor/enshrined/svg-sanitize/src/Sanitizer.php
@@ -421,7 +421,7 @@
* Such as xlink:href when the xlink namespace isn't imported.
* We have to do this as the link is still ran in this case.
*/
- if (false !== strpos($attrName, 'href')) {
+ if (false !== stripos($attrName, 'href')) {
$href = $element->getAttribute($attrName);
if (false === $this->isHrefSafeValue($href)) {
$element->removeAttribute($attrName);
@@ -453,14 +453,17 @@
*/
protected function cleanXlinkHrefs(DOMElement $element)
{
- $xlinks = $element->getAttributeNS('http://www.w3.org/1999/xlink', 'href');
- if (false === $this->isHrefSafeValue($xlinks)) {
- $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
- $this->xmlIssues[] = array(
- 'message' => 'Suspicious attribute 'href'',
- 'line' => $element->getLineNo(),
- );
+ foreach ($element->attributes as $attribute) {
+ // remove attributes with unexpected namespace prefix, e.g. `XLinK:href` (instead of `xlink:href`)
+ if ($attribute->prefix === '' && strtolower($attribute->nodeName) === 'xlink:href') {
+ $element->removeAttribute($attribute->nodeName);
+ $this->xmlIssues[] = array(
+ 'message' => sprintf('Unexpected attribute '%s'', $attribute->nodeName),
+ 'line' => $element->getLineNo(),
+ );
+ }
}
+ $this->cleanHrefAttributes($element, 'xlink');
}
/**
@@ -470,13 +473,33 @@
*/
protected function cleanHrefs(DOMElement $element)
{
- $href = $element->getAttribute('href');
- if (false === $this->isHrefSafeValue($href)) {
- $element->removeAttribute('href');
- $this->xmlIssues[] = array(
- 'message' => 'Suspicious attribute 'href'',
- 'line' => $element->getLineNo(),
- );
+ $this->cleanHrefAttributes($element);
+ }
+
+ protected function cleanHrefAttributes(DOMElement $element, string $prefix = ''): void
+ {
+ $relevantAttributes = array_filter(
+ iterator_to_array($element->attributes),
+ static function (DOMAttr $attr) use ($prefix) {
+ return strtolower($attr->name) === 'href' && strtolower($attr->prefix) === $prefix;
+ }
+ );
+ foreach ($relevantAttributes as $attribute) {
+ if (!$this->isHrefSafeValue($attribute->value)) {
+ $element->removeAttribute($attribute->nodeName);
+ $this->xmlIssues[] = array(
+ 'message' => sprintf('Suspicious attribute '%s'', $attribute->nodeName),
+ 'line' => $element->getLineNo(),
+ );
+ continue;
+ }
+ // in case the attribute name is `HrEf`/`xlink:HrEf`, adjust it to `href`/`xlink:href`
+ if (!in_array($attribute->nodeName, $this->allowedAttrs, true)
+ && in_array(strtolower($attribute->nodeName), $this->allowedAttrs, true)
+ ) {
+ $element->removeAttribute($attribute->nodeName);
+ $element->setAttribute(strtolower($attribute->nodeName), $attribute->value);
+ }
}
}