Skip to content

Commit cb4557b

Browse files
Merge pull request #57420 from nextcloud/backport/57414/stable32
[stable32] fix(l10n): Fix language selection
2 parents d0da446 + eff3bb4 commit cb4557b

File tree

2 files changed

+47
-10
lines changed

2 files changed

+47
-10
lines changed

lib/private/L10N/Factory.php

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,8 @@ public function __construct(
9494
public function get($app, $lang = null, $locale = null) {
9595
return new LazyL10N(function () use ($app, $lang, $locale) {
9696
$app = $this->appManager->cleanAppId($app);
97-
if ($lang !== null) {
98-
$lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
99-
}
100-
101-
$forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
97+
$lang = $this->cleanLanguage($lang);
98+
$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
10299
if (is_string($forceLang)) {
103100
$lang = $forceLang;
104101
}
@@ -128,6 +125,29 @@ public function get($app, $lang = null, $locale = null) {
128125
});
129126
}
130127

128+
/**
129+
* Remove some invalid characters before using a string as a language
130+
*
131+
* @psalm-taint-escape callable
132+
* @psalm-taint-escape cookie
133+
* @psalm-taint-escape file
134+
* @psalm-taint-escape has_quotes
135+
* @psalm-taint-escape header
136+
* @psalm-taint-escape html
137+
* @psalm-taint-escape include
138+
* @psalm-taint-escape ldap
139+
* @psalm-taint-escape shell
140+
* @psalm-taint-escape sql
141+
* @psalm-taint-escape unserialize
142+
*/
143+
private function cleanLanguage(?string $lang): ?string {
144+
if ($lang === null) {
145+
return null;
146+
}
147+
$lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
148+
return str_replace('..', '', $lang);
149+
}
150+
131151
/**
132152
* Check that $lang is an existing language and not null, otherwise return the language to use instead
133153
*
@@ -160,7 +180,7 @@ private function validateLanguage(string $app, ?string $lang): string {
160180
*/
161181
public function findLanguage(?string $appId = null): string {
162182
// Step 1: Forced language always has precedence over anything else
163-
$forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
183+
$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
164184
if (is_string($forceLang)) {
165185
$this->requestLanguage = $forceLang;
166186
}
@@ -217,7 +237,7 @@ public function findLanguage(?string $appId = null): string {
217237

218238
public function findGenericLanguage(?string $appId = null): string {
219239
// Step 1: Forced language always has precedence over anything else
220-
$forcedLanguage = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
240+
$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
221241
if ($forcedLanguage !== false) {
222242
return $forcedLanguage;
223243
}
@@ -412,7 +432,8 @@ public function getUserLanguage(?IUser $user = null): string {
412432
return $language;
413433
}
414434

415-
if (($forcedLanguage = $this->request->getParam('forceLanguage')) !== null) {
435+
$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage'));
436+
if ($forcedLanguage !== null) {
416437
return $forcedLanguage;
417438
}
418439

@@ -426,7 +447,7 @@ public function getUserLanguage(?IUser $user = null): string {
426447
}
427448
}
428449

429-
return $this->request->getParam('forceLanguage') ?? $this->config->getSystemValueString('default_language', 'en');
450+
return $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValueString('default_language', 'en');
430451
}
431452

432453
/**
@@ -452,7 +473,7 @@ public function localeExists($locale) {
452473
* @throws LanguageNotFoundException
453474
*/
454475
private function getLanguageFromRequest(?string $app = null): string {
455-
$header = $this->request->getHeader('ACCEPT_LANGUAGE');
476+
$header = $this->cleanLanguage($this->request->getHeader('ACCEPT_LANGUAGE'));
456477
if ($header !== '') {
457478
$available = $this->findAvailableLanguages($app);
458479

tests/lib/L10N/FactoryTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use OCP\IUser;
2121
use OCP\IUserSession;
2222
use OCP\L10N\ILanguageIterator;
23+
use PHPUnit\Framework\Attributes\DataProvider;
2324
use PHPUnit\Framework\MockObject\MockObject;
2425
use Test\TestCase;
2526

@@ -90,6 +91,21 @@ protected function getFactory(array $methods = [], $mockRequestGetHeaderMethod =
9091
return new Factory($this->config, $this->request, $this->userSession, $this->cacheFactory, $this->serverRoot, $this->appManager);
9192
}
9293

94+
public static function dataCleanLanguage(): array {
95+
return [
96+
'null shortcut' => [null, null],
97+
'default language' => ['de', 'de'],
98+
'malicious language' => ['de/../fr', 'defr'],
99+
'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'],
100+
];
101+
}
102+
103+
#[DataProvider('dataCleanLanguage')]
104+
public function testCleanLanguage(?string $lang, ?string $expected): void {
105+
$factory = $this->getFactory();
106+
$this->assertSame($expected, self::invokePrivate($factory, 'cleanLanguage', [$lang]));
107+
}
108+
93109
public static function dataFindAvailableLanguages(): array {
94110
return [
95111
[null],

0 commit comments

Comments
 (0)