Plugin Directory

Changeset 3421231


Ignore:
Timestamp:
12/16/2025 04:07:13 PM (2 months ago)
Author:
rosell.dk
Message:

0.25.10. Protects the config files from being accessed directly on NGINX by renaming them to randomized names. It also creates an index.php to prevent directory listing

Location:
webp-express
Files:
10 added
17 edited
1 copied

Legend:

Unmodified
Added
Removed
  • webp-express/tags/0.25.10/README.txt

    r3066149 r3421231  
    44Tags: webp, images, performance
    55Requires at least: 4.0
    6 Tested up to: 6.5
    7 Stable tag: 0.25.9
     6Tested up to: 6.9
     7Stable tag: 0.25.10
    88Requires PHP: 5.6
    99License: GPLv3
     
    174174**Persons who recently contributed with [ko-fi](https://ko-fi.com/rosell) - Thanks!**
    175175
    176 * 3 Nov: Tobi
    177 * 5 Nov: Anon
    178 * 18 Nov: Oleksii
    179 * 20 Feb: Assen Kovatchev
    180 * 22 Feb: Peter
    181 * 29 Feb: Luis Méndez Alejo
    182 * 5 Mar: tomottoe
    183 * 9 Mar: La Braud
     176* 9 Aug: Tanzi
     177* 3 Jul: Jen
     178* 26 Jun: Per
     179* 16 May: Erick Danzer
     180* 8 May: Mike
     181* 31 May: parallactic
     182* 14 May: Gitte Rebsdorf
     183* 9 May: La Braud
    184184
    185185**Persons who contributed with extra generously amounts of coffee / lifetime backing (>30$) - thanks!:**
     
    196196* Brian Laursen ($50)
    197197* Dimitris Vayenas ($50)
    198 
    199 **Persons currently backing the project via GitHub Sponsors or patreon - Thanks!**
    200 
    201 * [Mathieu Gollain-Dupont](https://www.linkedin.com/in/mathieu-gollain-dupont-9938a4a/)
    202198
    203199== Frequently Asked Questions ==
     
    819815== Changelog ==
    820816
     817= 0.25.10 =
     818(released 15 December 2025)
     819* Security fix: Config file was exposed on systems running on NGINX. Herr Patrick Müller from Switzerland for creating a patch as well as Rune Philosof from Denmark for improving it. Some credit also goes to myself for perfecting the patch. Sorry for slacking on the maintenance. There are good reasons for this, but I can and will do better in the future
     820
    821821= 0.25.9 =
    822822(released 7 April 2024)
     
    868868== Upgrade Notice ==
    869869
     870= 0.25.10 =
     871* Security fix. The config file could be exposed on NGINX
     872
    870873= 0.25.9 =
    871874* Fixed ewww conversion method after ewww API change
  • webp-express/tags/0.25.10/lib/classes/AdminInit.php

    r2629766 r3421231  
    2626    {
    2727        // When an update requires a migration, the number should be increased
    28         define('WEBPEXPRESS_MIGRATION_VERSION', '14');
     28        define('WEBPEXPRESS_MIGRATION_VERSION', '15');
    2929
    3030        if (WEBPEXPRESS_MIGRATION_VERSION != Option::getOption('webp-express-migration-version', 0)) {
     
    3434
    3535        // uncomment next line to test-run a migration
    36         //include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate14.php';
     36        //include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate15.php';
    3737    }
    3838
  • webp-express/tags/0.25.10/lib/classes/Config.php

    r2629766 r3421231  
    99
    1010    /**
     11     * Migrate old predictable config files to randomized names (CVE-2025-11379 fix)
     12     * This should be called during plugin upgrade or early on admin load
     13     */
     14    public static function migrateConfigFiles()
     15    {
     16        $oldConfigFile = Paths::getOldConfigFileName();
     17        $newConfigFile = Paths::getConfigFileName();
     18        $oldWodFile = Paths::getOldWodOptionsFileName();
     19        $newWodFile = Paths::getWodOptionsFileName();
     20
     21        $migrated = false;
     22
     23        // Migrate config.json if it exists and new one doesn't
     24        if (file_exists($oldConfigFile) && !file_exists($newConfigFile)) {
     25            if (@rename($oldConfigFile, $newConfigFile)) {
     26                $migrated = true;
     27            } elseif (@copy($oldConfigFile, $newConfigFile)) {
     28                @unlink($oldConfigFile);
     29                $migrated = true;
     30            }
     31        }
     32
     33        // Migrate wod-options.json if it exists and new one doesn't
     34        if (file_exists($oldWodFile) && !file_exists($newWodFile)) {
     35            if (@rename($oldWodFile, $newWodFile)) {
     36                $migrated = true;
     37            } elseif (@copy($oldWodFile, $newWodFile)) {
     38                @unlink($oldWodFile);
     39                $migrated = true;
     40            }
     41        }
     42
     43        // Clean up any remaining old files (in case new files already existed)
     44        if (file_exists($oldConfigFile) && file_exists($newConfigFile)) {
     45            @unlink($oldConfigFile);
     46        }
     47        if (file_exists($oldWodFile) && file_exists($newWodFile)) {
     48            @unlink($oldWodFile);
     49        }
     50
     51        return $migrated;
     52    }
     53
     54    /**
     55     * Check and perform config file migration if needed (CVE-2025-11379 fix)
     56     * This is called early on admin load to ensure migration happens even if options page is never visited
     57     *
     58     * @return boolean true if its either already migrated or it was migratede successfully or there is no need for migration (in case one starts from newer WebP Express version). false if migration fails
     59     */
     60    public static function checkAndMigrateConfigIfNeeded()
     61    {
     62        // Only run once per request to avoid performance impact
     63        static $checked = false;
     64        if ($checked) {
     65            return true;
     66        }
     67        $checked = true;
     68
     69        // Check if migration flag is set to avoid checking filesystem on every request
     70        if (Option::getOption('webp-express-config-migrated-cve-2025-11379', false)) {
     71            return true;
     72        }
     73
     74        // Check if old files exist
     75        $oldConfigFile = Paths::getOldConfigFileName();
     76        $oldWodFile = Paths::getOldWodOptionsFileName();
     77
     78        if (file_exists($oldConfigFile) || file_exists($oldWodFile)) {
     79            if (self::migrateConfigFiles()) {
     80                Option::updateOption('webp-express-config-migrated-cve-2025-11379', true, true);
     81                return true;
     82            }
     83            return false;
     84        } else {
     85            // No old files found, mark as migrated to avoid future checks
     86            Option::updateOption('webp-express-config-migrated-cve-2025-11379', true, true);
     87            return true;
     88        }
     89    }
     90
     91    /**
    1192     *  @return  object|false   Returns config object if config file exists and can be read. Otherwise it returns false
    1293     */
    1394    public static function loadConfig()
    1495    {
     96        // Attempt migration before loading config
     97        self::checkAndMigrateConfigIfNeeded();
     98
    1599        return FileHelper::loadJSONOptions(Paths::getConfigFileName());
    16100    }
  • webp-express/tags/0.25.10/lib/classes/ConvertHelperIndependent.php

    r3066146 r3421231  
    572572
    573573        // TODO: Put version number somewhere else. Ie \WebPExpress\VersionNumber::version
    574         $text = 'WebP Express 0.25.9. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
     574        $text = 'WebP Express 0.25.10. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
    575575
    576576        $logFile = self::getLogFilename($source, $logDir);
  • webp-express/tags/0.25.10/lib/classes/HTAccessRules.php

    r2629766 r3421231  
    435435
    436436
     437        $configHash = Paths::getConfigHash();
     438
    437439        if (self::$useDocRootForStructuringCacheDir) {
    438440            /*
     
    447449            if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    448450                $flags[] = 'E=DESTINATIONREL:' . self::$htaccessDirRelToDocRoot . '/$0';
    449             }
    450             if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    451                 $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
     451                $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel() . '/$0';
     452                $flags[] = 'E=HASH:' . $configHash;
    452453            }
    453454            $flags[] = 'NC';
     
    460461            if (!self::$passThroughEnvVarDefinitelyAvailable) {
    461462                $params[] = "wp-content=" . Paths::getContentDirRel();
     463                $params[] = "hash=" . $configHash;
    462464            }
    463465
     
    486488                $flags[] = 'E=WE_DESTINATION_REL_HTACCESS:$0';
    487489                $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;    // this will btw either be "uploads" or "cache"
     490                $flags[] = 'E=HASH:' . $configHash;
    488491            }
    489492            $flags[] = 'NC';  // case-insensitive match (so file extension can be jpg, JPG or even jPg)
     
    495498                $params[] = 'xdestination-rel-htaccess=x$0';
    496499                $params[] = 'htaccess-id=' . self::$htaccessDir;
     500                $params[] = "hash=" . $configHash;
    497501            }
    498502
     
    633637        }
    634638
     639        $configHash = Paths::getConfigHash();
    635640        if (self::$useDocRootForStructuringCacheDir) {
    636641            /*
     
    653658            if (!self::$passThroughEnvVarDefinitelyAvailable) {
    654659                $params[] = "wp-content=" . Paths::getContentDirRel();
     660                $params[] = "hash=" . $configHash;
    655661            }
    656662            if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    657663                $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
     664                $flags[] = 'E=HASH:' . $configHash;
    658665            }
    659666
     
    684691                $flags[] = 'E=WE_WP_CONTENT_REL_TO_WE_PLUGIN_DIR:' . Paths::getContentDirRelToWebPExpressPluginDir();
    685692                $flags[] = 'E=WE_SOURCE_REL_HTACCESS:$0';
    686                 $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;    // this will btw be one of the image roots. It will not be "cache"
     693                $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;
     694                $flags[] = 'E=HASH:' . $configHash;  // this will btw be one of the image roots. It will not be "cache"
    687695            }
    688696            $flags[] = 'NC';  // case-insensitive match (so file extension can be jpg, JPG or even jPg)
     
    694702                $params[] = 'xsource-rel-htaccess=x$0';
    695703                $params[] = 'htaccess-id=' . self::$htaccessDir;
     704                $params[] = "hash=" . $configHash;
    696705            }
    697706
  • webp-express/tags/0.25.10/lib/classes/Paths.php

    r2981234 r3421231  
    428428    {
    429429        return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getConfigDirAbs());
     430    }
     431
     432    /**
     433     * Get or create a random hash for config filename obfuscation (CVE-2025-11379 fix)
     434     * This prevents predictable config file access on Nginx servers
     435     */
     436    public static function getConfigHash()
     437    {
     438        $hash = \WebPExpress\Option::getOption('webp-express-config-hash', false);
     439        if (!$hash) {
     440            // Generate a cryptographically secure random hash
     441            if (function_exists('random_bytes')) {
     442                $hash = bin2hex(random_bytes(16));
     443            } else {
     444                // Fallback for older PHP versions
     445                $hash = md5(uniqid(mt_rand(), true) . microtime(true));
     446            }
     447            \WebPExpress\Option::updateOption('webp-express-config-hash', $hash, true);
     448        }
     449        return $hash;
    430450    }
    431451
     
    450470            );
    451471            @chmod($configDir . '/.htaccess', 0664);
     472
     473            // Additional protection for Nginx: PHP-based access control (CVE-2025-11379 fix)
     474            @file_put_contents(rtrim($configDir . '/') . '/index.php', <<<'PHP'
     475<?php
     476// Prevent direct access to config files on Nginx (CVE-2025-11379 fix)
     477if (!defined('ABSPATH')) {
     478    http_response_code(403);
     479    die('Direct access forbidden');
     480}
     481PHP
     482            );
     483            @chmod($configDir . '/index.php', 0644);
    452484        }
    453485        return is_dir($configDir);
     
    456488    public static function getConfigFileName()
    457489    {
     490        // Use randomized filename to prevent predictable access on Nginx (CVE-2025-11379 fix)
     491        $hash = self::getConfigHash();
     492        return self::getConfigDirAbs() . '/config.' . $hash . '.json';
     493    }
     494
     495    public static function getWodOptionsFileName()
     496    {
     497        // Use randomized filename to prevent predictable access on Nginx (CVE-2025-11379 fix)
     498        $hash = self::getConfigHash();
     499        return self::getConfigDirAbs() . '/wod-options.' . $hash . '.json';
     500    }
     501
     502    /**
     503     * Get old predictable config filename for migration purposes
     504     */
     505    public static function getOldConfigFileName()
     506    {
    458507        return self::getConfigDirAbs() . '/config.json';
    459508    }
    460509
    461     public static function getWodOptionsFileName()
     510    /**
     511     * Get old predictable wod-options filename for migration purposes
     512     */
     513    public static function getOldWodOptionsFileName()
    462514    {
    463515        return self::getConfigDirAbs() . '/wod-options.json';
  • webp-express/tags/0.25.10/lib/classes/WodConfigLoader.php

    r2626928 r3421231  
    183183    protected static function loadConfig() {
    184184
     185        $hash = self::getEnvPassedInRewriteRule('HASH');
     186        if ($hash === false) {
     187            // Passed in QS?
     188            if (isset($_GET['hash'])) {
     189                $hash = $_GET['hash'];
     190            } else {
     191                // In case above fails, fall back to standard location
     192                $hash = '';
     193            }
     194        }
     195        $filename = '';
     196
     197        if ($hash == '') {
     198            $filename = 'wod-options.json';
     199        } else {
     200            $filename = 'wod-options.' . $hash . '.json';
     201        }
     202
    185203        $usingDocRoot = !(
    186204            isset($_GET['xwp-content-rel-to-we-plugin-dir']) ||
     
    218236        self::$checking = 'config file';
    219237
    220         $configFilename = self::$webExpressContentDirAbs . '/config/wod-options.json';
     238        $configFilename = self::$webExpressContentDirAbs . '/config/' . $filename;
    221239        if (!file_exists($configFilename)) {
    222             throw new \Exception('Configuration file was not found (wod-options.json)');
     240            throw new \Exception('Configuration file was not found (wod-options.some-hash.json)');
    223241        }
    224242
  • webp-express/tags/0.25.10/webp-express.php

    r3066146 r3421231  
    44 * Plugin URI: https://github.com/rosell-dk/webp-express
    55 * Description: Serve autogenerated WebP images instead of jpeg/png to browsers that supports WebP. Works on anything (media library images, galleries, theme images etc).
    6  * Version: 0.25.9
     6 * Version: 0.25.10
    77 * Author: Bjørn Rosell
    88 * Author URI: https://www.bitwise-it.dk
     
    3131
    3232if (is_admin()) {
     33    // Initialize admin hooks
    3334    \WebPExpress\AdminInit::init();
    3435}
  • webp-express/trunk/README.txt

    r3066149 r3421231  
    44Tags: webp, images, performance
    55Requires at least: 4.0
    6 Tested up to: 6.5
    7 Stable tag: 0.25.9
     6Tested up to: 6.9
     7Stable tag: 0.25.10
    88Requires PHP: 5.6
    99License: GPLv3
     
    174174**Persons who recently contributed with [ko-fi](https://ko-fi.com/rosell) - Thanks!**
    175175
    176 * 3 Nov: Tobi
    177 * 5 Nov: Anon
    178 * 18 Nov: Oleksii
    179 * 20 Feb: Assen Kovatchev
    180 * 22 Feb: Peter
    181 * 29 Feb: Luis Méndez Alejo
    182 * 5 Mar: tomottoe
    183 * 9 Mar: La Braud
     176* 9 Aug: Tanzi
     177* 3 Jul: Jen
     178* 26 Jun: Per
     179* 16 May: Erick Danzer
     180* 8 May: Mike
     181* 31 May: parallactic
     182* 14 May: Gitte Rebsdorf
     183* 9 May: La Braud
    184184
    185185**Persons who contributed with extra generously amounts of coffee / lifetime backing (>30$) - thanks!:**
     
    196196* Brian Laursen ($50)
    197197* Dimitris Vayenas ($50)
    198 
    199 **Persons currently backing the project via GitHub Sponsors or patreon - Thanks!**
    200 
    201 * [Mathieu Gollain-Dupont](https://www.linkedin.com/in/mathieu-gollain-dupont-9938a4a/)
    202198
    203199== Frequently Asked Questions ==
     
    819815== Changelog ==
    820816
     817= 0.25.10 =
     818(released 15 December 2025)
     819* Security fix: Config file was exposed on systems running on NGINX. Herr Patrick Müller from Switzerland for creating a patch as well as Rune Philosof from Denmark for improving it. Some credit also goes to myself for perfecting the patch. Sorry for slacking on the maintenance. There are good reasons for this, but I can and will do better in the future
     820
    821821= 0.25.9 =
    822822(released 7 April 2024)
     
    868868== Upgrade Notice ==
    869869
     870= 0.25.10 =
     871* Security fix. The config file could be exposed on NGINX
     872
    870873= 0.25.9 =
    871874* Fixed ewww conversion method after ewww API change
  • webp-express/trunk/docs/publishing.md

    r3066149 r3421231  
    112112```
    113113cd svn
    114 svn cp trunk tags/0.25.9       (this will copy trunk into a new tag)
     114svn cp trunk tags/0.25.10       (this will copy trunk into a new tag)
    115115```
    116116
    117117And commit!
    118118```
    119 svn ci -m '0.25.9'
     119svn ci -m '0.25.10. Fixed '
    120120```
    121121
  • webp-express/trunk/lib/classes/AdminInit.php

    r2629766 r3421231  
    2626    {
    2727        // When an update requires a migration, the number should be increased
    28         define('WEBPEXPRESS_MIGRATION_VERSION', '14');
     28        define('WEBPEXPRESS_MIGRATION_VERSION', '15');
    2929
    3030        if (WEBPEXPRESS_MIGRATION_VERSION != Option::getOption('webp-express-migration-version', 0)) {
     
    3434
    3535        // uncomment next line to test-run a migration
    36         //include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate14.php';
     36        // include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate15.php';
    3737    }
    3838
  • webp-express/trunk/lib/classes/Config.php

    r2629766 r3421231  
    99
    1010    /**
     11     * Migrate old predictable config files to randomized names (CVE-2025-11379 fix)
     12     * This should be called during plugin upgrade or early on admin load
     13     */
     14    public static function migrateConfigFiles()
     15    {
     16        $oldConfigFile = Paths::getOldConfigFileName();
     17        $newConfigFile = Paths::getConfigFileName();
     18        $oldWodFile = Paths::getOldWodOptionsFileName();
     19        $newWodFile = Paths::getWodOptionsFileName();
     20
     21        $migrated = false;
     22
     23        // Migrate config.json if it exists and new one doesn't
     24        if (file_exists($oldConfigFile) && !file_exists($newConfigFile)) {
     25            if (@rename($oldConfigFile, $newConfigFile)) {
     26                $migrated = true;
     27            } elseif (@copy($oldConfigFile, $newConfigFile)) {
     28                @unlink($oldConfigFile);
     29                $migrated = true;
     30            }
     31        }
     32
     33        // Migrate wod-options.json if it exists and new one doesn't
     34        if (file_exists($oldWodFile) && !file_exists($newWodFile)) {
     35            if (@rename($oldWodFile, $newWodFile)) {
     36                $migrated = true;
     37            } elseif (@copy($oldWodFile, $newWodFile)) {
     38                @unlink($oldWodFile);
     39                $migrated = true;
     40            }
     41        }
     42
     43        // Clean up any remaining old files (in case new files already existed)
     44        if (file_exists($oldConfigFile) && file_exists($newConfigFile)) {
     45            @unlink($oldConfigFile);
     46        }
     47        if (file_exists($oldWodFile) && file_exists($newWodFile)) {
     48            @unlink($oldWodFile);
     49        }
     50
     51        return $migrated;
     52    }
     53
     54    /**
     55     * Check and perform config file migration if needed (CVE-2025-11379 fix)
     56     * This is called early on admin load to ensure migration happens even if options page is never visited
     57     *
     58     * @return boolean true if its either already migrated or it was migratede successfully or there is no need for migration (in case one starts from newer WebP Express version). false if migration fails
     59     */
     60    public static function checkAndMigrateConfigIfNeeded()
     61    {
     62        // Only run once per request to avoid performance impact
     63        static $checked = false;
     64        if ($checked) {
     65            return true;
     66        }
     67        $checked = true;
     68
     69        // Check if migration flag is set to avoid checking filesystem on every request
     70        if (Option::getOption('webp-express-config-migrated-cve-2025-11379', false)) {
     71            return true;
     72        }
     73
     74        // Check if old files exist
     75        $oldConfigFile = Paths::getOldConfigFileName();
     76        $oldWodFile = Paths::getOldWodOptionsFileName();
     77
     78        if (file_exists($oldConfigFile) || file_exists($oldWodFile)) {
     79            if (self::migrateConfigFiles()) {
     80                Option::updateOption('webp-express-config-migrated-cve-2025-11379', true, true);
     81                return true;
     82            }
     83            return false;
     84        } else {
     85            // No old files found, mark as migrated to avoid future checks
     86            Option::updateOption('webp-express-config-migrated-cve-2025-11379', true, true);
     87            return true;
     88        }
     89    }
     90
     91    /**
    1192     *  @return  object|false   Returns config object if config file exists and can be read. Otherwise it returns false
    1293     */
    1394    public static function loadConfig()
    1495    {
     96        // Attempt migration before loading config
     97        self::checkAndMigrateConfigIfNeeded();
     98
    1599        return FileHelper::loadJSONOptions(Paths::getConfigFileName());
    16100    }
  • webp-express/trunk/lib/classes/ConvertHelperIndependent.php

    r3066146 r3421231  
    572572
    573573        // TODO: Put version number somewhere else. Ie \WebPExpress\VersionNumber::version
    574         $text = 'WebP Express 0.25.9. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
     574        $text = 'WebP Express 0.25.10. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
    575575
    576576        $logFile = self::getLogFilename($source, $logDir);
  • webp-express/trunk/lib/classes/HTAccessRules.php

    r2629766 r3421231  
    435435
    436436
     437        $configHash = Paths::getConfigHash();
     438
    437439        if (self::$useDocRootForStructuringCacheDir) {
    438440            /*
     
    447449            if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    448450                $flags[] = 'E=DESTINATIONREL:' . self::$htaccessDirRelToDocRoot . '/$0';
    449             }
    450             if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    451                 $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
     451                $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel() . '/$0';
     452                $flags[] = 'E=HASH:' . $configHash;
    452453            }
    453454            $flags[] = 'NC';
     
    460461            if (!self::$passThroughEnvVarDefinitelyAvailable) {
    461462                $params[] = "wp-content=" . Paths::getContentDirRel();
     463                $params[] = "hash=" . $configHash;
    462464            }
    463465
     
    486488                $flags[] = 'E=WE_DESTINATION_REL_HTACCESS:$0';
    487489                $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;    // this will btw either be "uploads" or "cache"
     490                $flags[] = 'E=HASH:' . $configHash;
    488491            }
    489492            $flags[] = 'NC';  // case-insensitive match (so file extension can be jpg, JPG or even jPg)
     
    495498                $params[] = 'xdestination-rel-htaccess=x$0';
    496499                $params[] = 'htaccess-id=' . self::$htaccessDir;
     500                $params[] = "hash=" . $configHash;
    497501            }
    498502
     
    633637        }
    634638
     639        $configHash = Paths::getConfigHash();
    635640        if (self::$useDocRootForStructuringCacheDir) {
    636641            /*
     
    653658            if (!self::$passThroughEnvVarDefinitelyAvailable) {
    654659                $params[] = "wp-content=" . Paths::getContentDirRel();
     660                $params[] = "hash=" . $configHash;
    655661            }
    656662            if (!self::$passThroughEnvVarDefinitelyUnavailable) {
    657663                $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
     664                $flags[] = 'E=HASH:' . $configHash;
    658665            }
    659666
     
    684691                $flags[] = 'E=WE_WP_CONTENT_REL_TO_WE_PLUGIN_DIR:' . Paths::getContentDirRelToWebPExpressPluginDir();
    685692                $flags[] = 'E=WE_SOURCE_REL_HTACCESS:$0';
    686                 $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;    // this will btw be one of the image roots. It will not be "cache"
     693                $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir;
     694                $flags[] = 'E=HASH:' . $configHash;  // this will btw be one of the image roots. It will not be "cache"
    687695            }
    688696            $flags[] = 'NC';  // case-insensitive match (so file extension can be jpg, JPG or even jPg)
     
    694702                $params[] = 'xsource-rel-htaccess=x$0';
    695703                $params[] = 'htaccess-id=' . self::$htaccessDir;
     704                $params[] = "hash=" . $configHash;
    696705            }
    697706
  • webp-express/trunk/lib/classes/Paths.php

    r2981234 r3421231  
    430430    }
    431431
    432     public static function createConfigDirIfMissing()
     432    /**
     433     * Get or create a random hash for config filename obfuscation (CVE-2025-11379 fix)
     434     * This prevents predictable config file access on Nginx servers
     435     */
     436    public static function getConfigHash()
     437    {
     438        $hash = \WebPExpress\Option::getOption('webp-express-config-hash', false);
     439        if (!$hash) {
     440            // Generate a cryptographically secure random hash
     441            if (function_exists('random_bytes')) {
     442                $hash = bin2hex(random_bytes(16));
     443            } else {
     444                // Fallback for older PHP versions
     445                $hash = md5(uniqid(mt_rand(), true) . microtime(true));
     446            }
     447            \WebPExpress\Option::updateOption('webp-express-config-hash', $hash, true);
     448        }
     449        return $hash;
     450    }
     451
     452    // Only call if certain that config dir exists
     453    private static function doCreateIndexPHPInConfigDirIfMissing()
    433454    {
    434455        $configDir = self::getConfigDirAbs();
    435         // Using code from Wordfence bootstrap.php...
    436         // Why not simply use wp_mkdir_p ? - it sets the permissions to same as parent. Isn't that better?
    437         // or perhaps not... - Because we need write permissions in the config dir.
    438         if (!is_dir($configDir)) {
    439             @mkdir($configDir, 0775);
    440             @chmod($configDir, 0775);
    441             @file_put_contents(rtrim($configDir . '/') . '/.htaccess', <<<APACHE
     456        $indexPHPfilename = rtrim($configDir, '/') . '/index.php';
     457
     458        if (!@file_exists($indexPHPfilename)) {
     459          // Additional protection for Nginx: PHP-based access control (CVE-2025-11379 fix)
     460          @file_put_contents($indexPHPfilename, <<<'PHP'
     461<?php
     462// Prevent direct access to config files on Nginx (CVE-2025-11379 fix)
     463if (!defined('ABSPATH')) {
     464  http_response_code(403);
     465  die('Direct access forbidden');
     466}
     467PHP
     468          );
     469          @chmod($indexPHPfilename, 0644);
     470        }
     471    }
     472
     473    // Only call if certain that config dir exists
     474    private static function doCreateHTAccessInConfigDirIfMissing()
     475    {
     476        $configDir = self::getConfigDirAbs();
     477        $filename = rtrim($configDir, '/') . '/.htaccess';
     478
     479        if (!@file_exists($filename)) {
     480          // Additional protection for Nginx: PHP-based access control (CVE-2025-11379 fix)
     481          @file_put_contents(rtrim($configDir . '/') . '/.htaccess', <<<APACHE
    442482<IfModule mod_authz_core.c>
    443483Require all denied
     
    448488</IfModule>
    449489APACHE
    450             );
    451             @chmod($configDir . '/.htaccess', 0664);
     490          );
     491          @chmod($filename, 0664);
     492        }
     493    }
     494
     495    public static function createIndexPHPInConfigDirIfMissing()
     496    {
     497        $configDir = self::getConfigDirAbs();
     498
     499        if (is_dir($configDir)) {
     500          self::doCreateIndexPHPInConfigDirIfMissing();
     501        }
     502    }
     503
     504    public static function createConfigDirIfMissing()
     505    {
     506        $configDir = self::getConfigDirAbs();
     507        // Using code from Wordfence bootstrap.php...
     508        // Why not simply use wp_mkdir_p ? - it sets the permissions to same as parent. Isn't that better?
     509        // or perhaps not... - Because we need write permissions in the config dir.
     510        if (!is_dir($configDir)) {
     511            @mkdir($configDir, 0775);
     512            @chmod($configDir, 0775);
     513
     514            self::doCreateIndexPHPInConfigDirIfMissing();
     515            self::doCreateHTAccessInConfigDirIfMissing();
     516
    452517        }
    453518        return is_dir($configDir);
     
    456521    public static function getConfigFileName()
    457522    {
     523        // Use randomized filename to prevent predictable access on Nginx (CVE-2025-11379 fix)
     524        $hash = self::getConfigHash();
     525        return self::getConfigDirAbs() . '/config.' . $hash . '.json';
     526    }
     527
     528    public static function getWodOptionsFileName()
     529    {
     530        // Use randomized filename to prevent predictable access on Nginx (CVE-2025-11379 fix)
     531        $hash = self::getConfigHash();
     532        return self::getConfigDirAbs() . '/wod-options.' . $hash . '.json';
     533    }
     534
     535    /**
     536     * Get old predictable config filename for migration purposes
     537     */
     538    public static function getOldConfigFileName()
     539    {
    458540        return self::getConfigDirAbs() . '/config.json';
    459541    }
    460542
    461     public static function getWodOptionsFileName()
     543    /**
     544     * Get old predictable wod-options filename for migration purposes
     545     */
     546    public static function getOldWodOptionsFileName()
    462547    {
    463548        return self::getConfigDirAbs() . '/wod-options.json';
  • webp-express/trunk/lib/classes/WodConfigLoader.php

    r2626928 r3421231  
    183183    protected static function loadConfig() {
    184184
     185        $hash = self::getEnvPassedInRewriteRule('HASH');
     186        if ($hash === false) {
     187            // Passed in QS?
     188            if (isset($_GET['hash'])) {
     189                $hash = $_GET['hash'];
     190            } else {
     191                // In case above fails, fall back to standard location
     192                $hash = '';
     193            }
     194        }
     195        $filename = '';
     196
     197        if ($hash == '') {
     198            $filename = 'wod-options.json';
     199        } else {
     200            $filename = 'wod-options.' . $hash . '.json';
     201        }
     202
    185203        $usingDocRoot = !(
    186204            isset($_GET['xwp-content-rel-to-we-plugin-dir']) ||
     
    218236        self::$checking = 'config file';
    219237
    220         $configFilename = self::$webExpressContentDirAbs . '/config/wod-options.json';
     238        $configFilename = self::$webExpressContentDirAbs . '/config/' . $filename;
    221239        if (!file_exists($configFilename)) {
    222             throw new \Exception('Configuration file was not found (wod-options.json)');
     240            throw new \Exception('Configuration file was not found (wod-options.some-hash.json)');
    223241        }
    224242
  • webp-express/trunk/webp-express.php

    r3066146 r3421231  
    44 * Plugin URI: https://github.com/rosell-dk/webp-express
    55 * Description: Serve autogenerated WebP images instead of jpeg/png to browsers that supports WebP. Works on anything (media library images, galleries, theme images etc).
    6  * Version: 0.25.9
     6 * Version: 0.25.10
    77 * Author: Bjørn Rosell
    88 * Author URI: https://www.bitwise-it.dk
     
    3131
    3232if (is_admin()) {
     33    // Initialize admin hooks
    3334    \WebPExpress\AdminInit::init();
    3435}
Note: See TracChangeset for help on using the changeset viewer.