fix(l10n): Fix language selection

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/57419/head
Joas Schilling 4 months ago
parent 2896caf8d5
commit ae2bd13285
No known key found for this signature in database
GPG Key ID: F72FA5B49FFA96B0
  1. 41
      lib/private/L10N/Factory.php
  2. 17
      tests/lib/L10N/FactoryTest.php

@ -94,11 +94,8 @@ class Factory implements IFactory {
public function get($app, $lang = null, $locale = null) {
return new LazyL10N(function () use ($app, $lang, $locale) {
$app = $this->appManager->cleanAppId($app);
if ($lang !== null) {
$lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
}
$forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
$lang = $this->cleanLanguage($lang);
$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
if (is_string($forceLang)) {
$lang = $forceLang;
}
@ -130,6 +127,29 @@ class Factory implements IFactory {
});
}
/**
* Remove some invalid characters before using a string as a language
*
* @psalm-taint-escape callable
* @psalm-taint-escape cookie
* @psalm-taint-escape file
* @psalm-taint-escape has_quotes
* @psalm-taint-escape header
* @psalm-taint-escape html
* @psalm-taint-escape include
* @psalm-taint-escape ldap
* @psalm-taint-escape shell
* @psalm-taint-escape sql
* @psalm-taint-escape unserialize
*/
private function cleanLanguage(?string $lang): ?string {
if ($lang === null) {
return null;
}
$lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
return str_replace('..', '', $lang);
}
/**
* Find the best language
*
@ -139,7 +159,7 @@ class Factory implements IFactory {
*/
public function findLanguage(?string $appId = null): string {
// Step 1: Forced language always has precedence over anything else
$forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
if (is_string($forceLang)) {
$this->requestLanguage = $forceLang;
}
@ -196,7 +216,7 @@ class Factory implements IFactory {
public function findGenericLanguage(?string $appId = null): string {
// Step 1: Forced language always has precedence over anything else
$forcedLanguage = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false);
$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
if ($forcedLanguage !== false) {
return $forcedLanguage;
}
@ -391,7 +411,8 @@ class Factory implements IFactory {
return $language;
}
if (($forcedLanguage = $this->request->getParam('forceLanguage')) !== null) {
$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage'));
if ($forcedLanguage !== null) {
return $forcedLanguage;
}
@ -405,7 +426,7 @@ class Factory implements IFactory {
}
}
return $this->request->getParam('forceLanguage') ?? $this->config->getSystemValueString('default_language', 'en');
return $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValueString('default_language', 'en');
}
/**
@ -431,7 +452,7 @@ class Factory implements IFactory {
* @throws LanguageNotFoundException
*/
private function getLanguageFromRequest(?string $app = null): string {
$header = $this->request->getHeader('ACCEPT_LANGUAGE');
$header = $this->cleanLanguage($this->request->getHeader('ACCEPT_LANGUAGE'));
if ($header !== '') {
$available = $this->findAvailableLanguages($app);

@ -90,6 +90,23 @@ class FactoryTest extends TestCase {
return new Factory($this->config, $this->request, $this->userSession, $this->cacheFactory, $this->serverRoot, $this->appManager);
}
public static function dataCleanLanguage(): array {
return [
'null shortcut' => [null, null],
'default language' => ['de', 'de'],
'malicious language' => ['de/../fr', 'defr'],
'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'],
];
}
/**
* @dataProvider dataCleanLanguage
*/
public function testCleanLanguage(?string $lang, ?string $expected): void {
$factory = $this->getFactory();
$this->assertSame($expected, self::invokePrivate($factory, 'cleanLanguage', [$lang]));
}
public function dataFindAvailableLanguages(): array {
return [
[null],

Loading…
Cancel
Save