mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-21 18:25:02 -04:00
fix(locale): validate language_code against known locales to block path traversal (#4704)
* fix(locale): validate language_code against known locales to block path traversal postSaveLocale() stored language_code from user input with no allow-list validation, and it later flows into Language::setLocale()/load() where the locale segment is require()'d. An authenticated config-grant account could store a relative path (e.g. ../../public/uploads) and, combined with a planted file in public/uploads/, achieve unauthenticated RCE on the next request. Validate the submitted language against array_keys(get_languages()) before storing, and harden languageExists() to reject path separators and dot-dot sequences. Adds regression tests. * test(locale): give locale fixture valid reference-code min/max defaults * fix(locale): reject null bytes in languageExists guard A stored language_code containing a NUL byte passes the existing path-separator and parent-dir checks, then reaches file_exists(). On PHP 8.5+ file_exists() throws a ValueError for NUL-byte paths, which breaks configuration loading. Reject NUL bytes in the guard and add regression tests.
This commit is contained in:
1 parent
184918d914
commit
47aade5024
4 files changed
+117
-2
No files matched your search
@@ -506,7 +506,12 @@ class Config extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => false, 'message' => reset($errors)]);
|
||||
}
|
||||
|
||||
$exploded = explode(":", $this->request->getPost('language'));
|
||||
$language = $this->request->getPost('language');
|
||||
if (!in_array($language, array_keys(get_languages()), true)) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => 'Invalid language']);
|
||||
}
|
||||
|
||||
$exploded = explode(":", $language);
|
||||
$currency_symbol = $this->request->getPost('currency_symbol');
|
||||
$batch_save_data = [
|
||||
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||
|
||||
@@ -62,6 +62,10 @@ class Load_config
|
||||
|
||||
private function languageExists(string $languageCode): bool
|
||||
{
|
||||
if (strpbrk($languageCode, '/\\') !== false || str_contains($languageCode, '..') || str_contains($languageCode, "\0")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_exists(APPPATH . 'Language/' . $languageCode);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ class ConfigTest extends CIUnitTestCase
|
||||
private function baseLocalePayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'language' => 'en:English',
|
||||
'language' => 'en:english',
|
||||
'currency_symbol' => '$',
|
||||
'currency_code' => 'USD',
|
||||
'timezone' => 'UTC',
|
||||
@@ -151,6 +151,8 @@ class ConfigTest extends CIUnitTestCase
|
||||
'payment_options_order' => '',
|
||||
'cash_rounding_code' => '',
|
||||
'financial_year' => '1',
|
||||
'payment_reference_code_min' => '3',
|
||||
'payment_reference_code_max' => '20',
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
@@ -239,6 +241,61 @@ class ConfigTest extends CIUnitTestCase
|
||||
$this->assertFalse($result['success']);
|
||||
}
|
||||
|
||||
// ========== postSaveLocale: language_code allow-list (GHSA) ==========
|
||||
|
||||
public function testSaveLocale_RejectsPathTraversalLanguageCode(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
||||
'language' => '../../public/uploads:evil',
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('language', strtolower($result['message']));
|
||||
}
|
||||
|
||||
public function testSaveLocale_RejectsLanguageCodeWithBackslash(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
||||
'language' => '..\\..\\public\\uploads:evil',
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertFalse($result['success']);
|
||||
}
|
||||
|
||||
public function testSaveLocale_RejectsUnknownLanguage(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
||||
'language' => 'xx:nonexistent',
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertFalse($result['success']);
|
||||
}
|
||||
|
||||
public function testSaveLocale_RejectsCaseMismatchedLanguage(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
||||
'language' => 'en:English',
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertFalse($result['success']);
|
||||
}
|
||||
|
||||
// ========== postSaveGeneral: theme validation ==========
|
||||
|
||||
private function baseGeneralPayload(array $overrides = []): array
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Events;
|
||||
|
||||
use App\Events\Load_config;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use ReflectionMethod;
|
||||
|
||||
class Load_configTest extends CIUnitTestCase
|
||||
{
|
||||
private function languageExists(string $languageCode): bool
|
||||
{
|
||||
$instance = new Load_config();
|
||||
$method = new ReflectionMethod(Load_config::class, 'languageExists');
|
||||
$method->setAccessible(true);
|
||||
|
||||
return (bool) $method->invoke($instance, $languageCode);
|
||||
}
|
||||
|
||||
public function testLanguageExistsAcceptsValidCode(): void
|
||||
{
|
||||
$this->assertTrue($this->languageExists('en'));
|
||||
}
|
||||
|
||||
public function testLanguageExistsRejectsNullByte(): void
|
||||
{
|
||||
$this->assertFalse($this->languageExists("en\0"));
|
||||
}
|
||||
|
||||
public function testLanguageExistsRejectsNullByteOnly(): void
|
||||
{
|
||||
$this->assertFalse($this->languageExists("\0"));
|
||||
}
|
||||
|
||||
public function testLanguageExistsRejectsForwardSlashPath(): void
|
||||
{
|
||||
$this->assertFalse($this->languageExists('en/../../etc/passwd'));
|
||||
}
|
||||
|
||||
public function testLanguageExistsRejectsBackslashPath(): void
|
||||
{
|
||||
$this->assertFalse($this->languageExists('..\..\etc\passwd'));
|
||||
}
|
||||
|
||||
public function testLanguageExistsRejectsParentDir(): void
|
||||
{
|
||||
$this->assertFalse($this->languageExists('..'));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user