[TikiWiki-commits] [Git][tikiwiki/tiki][29.x] [FIX] Language custom.php: block path traversal and PHP injection in translation editor

"Espoir Baraka \(@esbarakabigega\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a82f9f291769_38e48b5867014@gitlab-sidekiq-low-urgency-cpu-bound-v2-788fd898f-ngnn4.mail>

Espoir Baraka pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki


Commits:
a325dfa5 by Espoir Baraka at 2026-08-17T14:02:23+02:00
[FIX] Language custom.php: block path traversal and PHP injection in translation editor
---
* [FIX] Language custom.php: block path traversal and PHP injection in translation editor
---
* [FIX] Language custom.php: block path traversal and PHP injection in translation editor
---
* [FIX] Language custom.php: block path traversal and PHP injection in translation editor
---
* Refactored language path retrieval to ensure custom PHP files are correctly resolved within the language directory.
* Introduced validation methods to prevent path traversal and ensure valid language codes.

(cherry picked from commit 17308932091c13c552c4951cb487aa25a6085139)

See merge request tikiwiki/tiki!10904

(cherry picked from commit a382e8be768d8b9653f9f554da2803fb29f35f48)

See merge request tikiwiki/tiki!10905

(cherry picked from commit ac64780c5cd63a92bc68e1eff1a33e2ee79c8951)

See merge request tikiwiki/tiki!10906

- - - - -


3 changed files:

- lib/core/Services/Language/Controller.php
- lib/core/Services/Language/Utilities.php
- + lib/test/core/Services/Language/UtilitiesTest.php


Changes:

=====================================
lib/core/Services/Language/Controller.php
=====================================
@@ -155,8 +155,7 @@ class Services_Language_Controller
         $languages = $this->utilities->getLanguages();
 
         //get custom php file location
-        $custom_php_file = $this->utilities->getLanguageDirectory($language);
-        $custom_php_file .= LANG_CUSTOM_PHP_BASENAME;
+        $custom_php_file = $this->utilities->getCustomPhpPath($language);
         if (! file_exists($custom_php_file)) {
             $custom_php_file = null;
         }


=====================================
lib/core/Services/Language/Utilities.php
=====================================
@@ -173,6 +173,20 @@ class Services_Language_Utilities
         return $languages;
     }
 
+    /**
+     * Ensure $language is a real on-disk language code (no path segments / traversal).
+     *
+     * @param string $language Language code (eg: en)
+     *
+     * @throws Services_Exception
+     */
+    public function assertValidLanguage($language)
+    {
+        if (! is_string($language) || $language === '' || ! Language::is_valid_language($language)) {
+            throw new Services_Exception(tr('Invalid language provided'), 400);
+        }
+    }
+
     /**
      * Get language directory generally and for a specific language too
      *
@@ -180,23 +194,60 @@ class Services_Language_Utilities
      *
      * @return string $langDir The directory path languages or to a language specifically
      *
+     * @throws Services_Exception
      */
     public function getLanguageDirectory($language = '')
     {
         $langDir = "lang/";
 
         if (! empty($language)) {
+            $this->assertValidLanguage($language);
             $langDir .= "$language/";
         }
 
         global $tikidomain;
         if (! empty($tikidomain)) {
+            // tikidomain is an internal install path segment; reject traversal if ever set from untrusted input
+            if (! is_string($tikidomain) || $tikidomain === '' || strpbrk($tikidomain, '/\\') !== false || str_contains($tikidomain, '..')) {
+                throw new Services_Exception(tr('Invalid language provided'), 400);
+            }
             $langDir .= "$tikidomain/";
         }
 
         return $langDir;
     }
 
+    /**
+     * Resolve custom.php path for a language and ensure it stays under lang/.
+     *
+     * @param string $language Language code (eg: en)
+     *
+     * @return string Relative path to custom.php
+     *
+     * @throws Services_Exception
+     */
+    public function getCustomPhpPath($language)
+    {
+        $this->assertValidLanguage($language);
+
+        $custom_file = $this->getLanguageDirectory($language) . LANG_CUSTOM_PHP_BASENAME;
+        $langRoot = realpath('lang');
+        $languageDir = realpath(dirname($custom_file));
+
+        if (
+            $langRoot === false
+            || $languageDir === false
+            || (
+                $languageDir !== $langRoot
+                && ! str_starts_with($languageDir, $langRoot . DIRECTORY_SEPARATOR)
+            )
+        ) {
+            throw new Services_Exception(tr('Invalid language provided'), 400);
+        }
+
+        return $custom_file;
+    }
+
     /**
      * Check if lang/ directory is readable generally and for a specific language too
      *
@@ -264,8 +315,7 @@ class Services_Language_Utilities
      */
     public function getCustomPhpTranslations($language)
     {
-        $custom_file = $this->getLanguageDirectory($language);
-        $custom_file .= LANG_CUSTOM_PHP_BASENAME;
+        $custom_file = $this->getCustomPhpPath($language);
 
         if (file_exists($custom_file)) {
             global $lang;
@@ -312,23 +362,19 @@ class Services_Language_Utilities
      */
     public function writeCustomPhpTranslations($language, $data)
     {
-        //prepare custom file path
-        $custom_file = $this->getLanguageDirectory($language);
-
-        //add file name
-        $custom_file .= LANG_CUSTOM_PHP_BASENAME;
-
-        //prepare php file
-        $custom_code = "<?php\r\n\$lang_custom = array(\r\n";
+        $custom_file = $this->getCustomPhpPath($language);
 
         if (! is_array($data)) {
             throw new Services_Exception(tr('String translation set is not an array'), 400);
         }
 
-        //add translations
+        //prepare php file — escape for double-quoted PHP strings (backslash before quote, etc.)
+        $custom_code = "<?php\r\n\$lang_custom = array(\r\n";
+
         foreach ($data as $from => $to) {
-            if (! empty($from)) {
-                $custom_code .= '"' . str_replace('"', '\\"', $from) . '" => "' . str_replace('"', '\\"', $to) . "\",\r\n";
+            if ($from !== '' && $from !== null) {
+                $custom_code .= '"' . Language::addPhpSlashes((string) $from) . '" => "'
+                    . Language::addPhpSlashes((string) $to) . "\",\r\n";
             }
         }
 


=====================================
lib/test/core/Services/Language/UtilitiesTest.php
=====================================
@@ -0,0 +1,77 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+namespace Tiki\Lib\Test\Core\Services\Language;
+
+use Services_Exception;
+use Services_Language_Utilities;
+use TikiTestCase;
+
+class UtilitiesTest extends TikiTestCase
+{
+    private Services_Language_Utilities $utilities;
+    private string $customFile;
+    private bool $hadCustomFile = false;
+    private string $previousCustomContents = '';
+
+    protected function setUp(): void
+    {
+        $this->utilities = new Services_Language_Utilities();
+        $this->customFile = 'lang/en/custom.php';
+        $this->hadCustomFile = file_exists($this->customFile);
+        if ($this->hadCustomFile) {
+            $this->previousCustomContents = file_get_contents($this->customFile);
+        }
+    }
+
+    protected function tearDown(): void
+    {
+        if ($this->hadCustomFile) {
+            file_put_contents($this->customFile, $this->previousCustomContents);
+        } elseif (file_exists($this->customFile)) {
+            unlink($this->customFile);
+        }
+    }
+
+    public function testRejectsPathTraversalLanguage(): void
+    {
+        $this->expectException(Services_Exception::class);
+        $this->utilities->assertValidLanguage('../temp');
+    }
+
+    public function testRejectsPathTraversalOnWrite(): void
+    {
+        $this->expectException(Services_Exception::class);
+        $this->utilities->writeCustomPhpTranslations('../temp', [
+            'k' => '\");echo \'PWNED\';?>',
+        ]);
+    }
+
+    public function testEscapesBackslashQuoteInjection(): void
+    {
+        $payload = '\");echo \'PWNED-CUSTOM-PHP\';?>';
+        $this->utilities->writeCustomPhpTranslations('en', [
+            'hello' => $payload,
+        ]);
+
+        $contents = file_get_contents($this->customFile);
+        // Escaped form inside the double-quoted PHP value is \\\");echo (three backslashes then quote)
+        $this->assertStringContainsString('\\\\\\");echo', $contents);
+
+        // Including the file must restore the literal payload, not execute injected PHP
+        $lang = [];
+        $lang_custom = null;
+        ob_start();
+        include $this->customFile;
+        $stdout = ob_get_clean();
+
+        $this->assertSame('', $stdout);
+        $this->assertIsArray($lang_custom);
+        $this->assertSame($payload, $lang_custom['hello']);
+        $this->assertSame($payload, $lang['hello']);
+    }
+}



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a325dfa57d86015475152a492f66ea0e796e8a19

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a325dfa57d86015475152a492f66ea0e796e8a19
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.