Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix psalm taint false-positives by small refactorings #50864

Merged
merged 1 commit into from
Feb 18, 2025
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
18 changes: 14 additions & 4 deletions apps/files_external/lib/Config/ConfigAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ public function __construct(
) {
}

/**
* @param class-string $class
* @return class-string<IObjectStore>
* @throws \InvalidArgumentException
* @psalm-taint-escape callable
*/
private function validateObjectStoreClassString(string $class): string {
if (!\is_subclass_of($class, IObjectStore::class)) {
throw new \InvalidArgumentException('Invalid object store');
}
return $class;
}

/**
* Process storage ready for mounting
*
Expand All @@ -51,10 +64,7 @@ private function prepareStorageConfig(StorageConfig &$storage, IUser $user): voi

$objectStore = $storage->getBackendOption('objectstore');
if ($objectStore) {
$objectClass = $objectStore['class'];
if (!is_subclass_of($objectClass, IObjectStore::class)) {
throw new \InvalidArgumentException('Invalid object store');
}
$objectClass = $this->validateObjectStoreClassString($objectStore['class']);
$storage->setBackendOption('objectstore', new $objectClass($objectStore));
}

Expand Down
27 changes: 26 additions & 1 deletion apps/provisioning_api/lib/Controller/AppsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public function __construct(
parent::__construct($appName, $request);
}

/**
* @throws \InvalidArgumentException
*/
protected function verifyAppId(string $app): string {
$cleanId = $this->appManager->cleanAppId($app);
if ($cleanId !== $app) {
throw new \InvalidArgumentException('Invalid app id given');
}
return $cleanId;
}

/**
* Get a list of installed apps
*
Expand Down Expand Up @@ -71,6 +82,11 @@ public function getApps(?string $filter = null): DataResponse {
* 200: App info returned
*/
public function getAppInfo(string $app): DataResponse {
try {
$app = $this->verifyAppId($app);
} catch (\InvalidArgumentException $e) {
throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
}
$info = $this->appManager->getAppInfo($app);
if (!is_null($info)) {
return new DataResponse($info);
Expand All @@ -91,7 +107,10 @@ public function getAppInfo(string $app): DataResponse {
#[PasswordConfirmationRequired]
public function enable(string $app): DataResponse {
try {
$app = $this->verifyAppId($app);
$this->appManager->enableApp($app);
} catch (\InvalidArgumentException $e) {
throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
} catch (AppPathNotFoundException $e) {
throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND);
}
Expand All @@ -103,12 +122,18 @@ public function enable(string $app): DataResponse {
*
* @param string $app ID of the app
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSException
*
* 200: App disabled successfully
*/
#[PasswordConfirmationRequired]
public function disable(string $app): DataResponse {
$this->appManager->disableApp($app);
try {
$app = $this->verifyAppId($app);
$this->appManager->disableApp($app);
} catch (\InvalidArgumentException $e) {
throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
}
return new DataResponse();
}
}
7 changes: 5 additions & 2 deletions apps/theming/lib/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public function generateRadioButton($color) {
* @return string|ISimpleFile path to app icon / file of logo
*/
public function getAppIcon($app) {
$app = str_replace(['\0', '/', '\\', '..'], '', $app);
$app = $this->appManager->cleanAppId($app);
try {
$appPath = $this->appManager->getAppPath($app);
$icon = $appPath . '/img/' . $app . '.svg';
Expand Down Expand Up @@ -228,7 +228,10 @@ public function getAppIcon($app) {
* @return string|false absolute path to image
*/
public function getAppImage($app, $image) {
$app = str_replace(['\0', '/', '\\', '..'], '', $app);
$app = $this->appManager->cleanAppId($app);
/**
* @psalm-taint-escape file
*/
$image = str_replace(['\0', '\\', '..'], '', $image);
if ($app === 'core') {
$icon = \OC::$SERVERROOT . '/core/img/' . $image;
Expand Down
36 changes: 0 additions & 36 deletions build/psalm-baseline-security.xml
Original file line number Diff line number Diff line change
@@ -1,41 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
<file src="apps/files_external/lib/Config/ConfigAdapter.php">
<TaintedCallable>
<code><![CDATA[$objectClass]]></code>
</TaintedCallable>
</file>
<file src="apps/theming/lib/IconBuilder.php">
<TaintedFile>
<code><![CDATA[$appIcon]]></code>
<code><![CDATA[$imageFile]]></code>
</TaintedFile>
</file>
<file src="lib/private/Config.php">
<TaintedHtml>
<code><![CDATA[$this->cache]]></code>
</TaintedHtml>
</file>
<file src="lib/private/Route/Router.php">
<TaintedCallable>
<code><![CDATA[$appNameSpace . '\\Controller\\' . basename($file->getPathname(), '.php')]]></code>
</TaintedCallable>
</file>
<file src="lib/private/Session/CryptoWrapper.php">
<TaintedCookie>
<code><![CDATA[$this->passphrase]]></code>
</TaintedCookie>
</file>
<file src="lib/private/Setup.php">
<TaintedFile>
<code><![CDATA[$dataDir]]></code>
</TaintedFile>
</file>
<file src="lib/private/Setup/Sqlite.php">
<TaintedFile>
<code><![CDATA[$sqliteFile]]></code>
</TaintedFile>
</file>
<file src="lib/public/DB/QueryBuilder/IQueryBuilder.php">
<TaintedSql>
<code><![CDATA[$column]]></code>
Expand Down
3 changes: 3 additions & 0 deletions core/Controller/SetupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ private function finishSetup(): void {
exit();
}

/**
* @psalm-taint-escape file we trust file path given in POST for setup
*/
public function loadAutoConfig(array $post): array {
if (file_exists($this->autoConfigFile)) {
$this->logger->info('Autoconfig file found, setting up Nextcloud…');
Expand Down
4 changes: 2 additions & 2 deletions lib/private/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private function readData() {
* @throws HintException If the config file cannot be written to
* @throws \Exception If no file lock can be acquired
*/
private function writeData() {
private function writeData(): void {
$this->checkReadOnly();

if (!is_file(\OC::$configDir . '/CAN_INSTALL') && !isset($this->cache['version'])) {
Expand All @@ -276,7 +276,7 @@ private function writeData() {
// Create a php file ...
$content = "<?php\n";
$content .= '$CONFIG = ';
$content .= var_export($this->cache, true);
$content .= var_export(self::trustSystemConfig($this->cache), true);
$content .= ";\n";

touch($this->configFilePath);
Expand Down
1 change: 0 additions & 1 deletion lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,6 @@ public function __construct($webRoot, \OC\Config $config) {
);

return new CryptoWrapper(
$c->get(\OCP\IConfig::class),
$c->get(ICrypto::class),
$c->get(ISecureRandom::class),
$request
Expand Down
49 changes: 15 additions & 34 deletions lib/private/Session/CryptoWrapper.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OC\Session;

use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\ICrypto;
Expand All @@ -30,37 +32,19 @@
* @package OC\Session
*/
class CryptoWrapper {
/** @var string */
public const COOKIE_NAME = 'oc_sessionPassphrase';

/** @var IConfig */
protected $config;
/** @var ISession */
protected $session;
/** @var ICrypto */
protected $crypto;
/** @var ISecureRandom */
protected $random;
/** @var string */
protected $passphrase;
protected string $passphrase;

/**
* @param IConfig $config
* @param ICrypto $crypto
* @param ISecureRandom $random
* @param IRequest $request
*/
public function __construct(IConfig $config,
ICrypto $crypto,
public function __construct(
protected ICrypto $crypto,
ISecureRandom $random,
IRequest $request) {
$this->crypto = $crypto;
$this->config = $config;
$this->random = $random;

if (!is_null($request->getCookie(self::COOKIE_NAME))) {
$this->passphrase = $request->getCookie(self::COOKIE_NAME);
} else {
$this->passphrase = $this->random->generate(128);
IRequest $request,
) {
$passphrase = $request->getCookie(self::COOKIE_NAME);
if ($passphrase === null) {
$passphrase = $random->generate(128);
$secureCookie = $request->getServerProtocol() === 'https';
// FIXME: Required for CI
if (!defined('PHPUNIT_RUN')) {
Expand All @@ -71,7 +55,7 @@ public function __construct(IConfig $config,

setcookie(
self::COOKIE_NAME,
$this->passphrase,
$passphrase,
[
'expires' => 0,
'path' => $webRoot,
Expand All @@ -83,13 +67,10 @@ public function __construct(IConfig $config,
);
}
}
$this->passphrase = $passphrase;
}

/**
* @param ISession $session
* @return ISession
*/
public function wrapSession(ISession $session) {
public function wrapSession(ISession $session): ISession {
if (!($session instanceof CryptoSessionData)) {
return new CryptoSessionData($session, $this->crypto, $this->passphrase);
}
Expand Down
Loading