Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions lib/private/L10N/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,8 @@ public function __construct(
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;
}
Expand Down Expand Up @@ -130,6 +127,29 @@ public function get($app, $lang = null, $locale = null) {
});
}

/**
* 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
*
Expand All @@ -139,7 +159,7 @@ public function get($app, $lang = null, $locale = null) {
*/
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;
}
Expand Down Expand Up @@ -196,7 +216,7 @@ public function findLanguage(?string $appId = null): string {

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;
}
Expand Down Expand Up @@ -391,7 +411,8 @@ public function getUserLanguage(?IUser $user = null): string {
return $language;
}

if (($forcedLanguage = $this->request->getParam('forceLanguage')) !== null) {
$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage'));
if ($forcedLanguage !== null) {
return $forcedLanguage;
}

Expand All @@ -405,7 +426,7 @@ public function getUserLanguage(?IUser $user = null): string {
}
}

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

/**
Expand All @@ -431,7 +452,7 @@ public function localeExists($locale) {
* @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);

Expand Down
17 changes: 17 additions & 0 deletions tests/lib/L10N/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ protected function getFactory(array $methods = [], $mockRequestGetHeaderMethod =
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],
Expand Down
Loading