From c5c3fc4589dcab39cd2a6e554b4fd57603ad66db Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 May 2026 06:42:55 +0200 Subject: [PATCH] fix(security): disable email-voting + ORI email leak + @self.id exposure (C2/C5/L1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C2: remove MailReplyHandler from background-jobs in info.xml and stop scheduling it in Application::boot() — email-voting is disabled until the feature is fully audited; removes unused IJobList import - C5: restrict ORI email field to Organization-typed resources only; was previously included for all types (meetings, motions, bodies), leaking any email field present on those objects to the public API - L1: fix serializeOri() id resolution to prefer entity uuid field over @self.id (internal OR metadata); @self.id must not be surfaced publicly - Fix pre-existing PHPStan: Application.php DI registrations were missing participantResolver param for MinutesController/VotingService/ LiveMeetingController, groupManager for TaskController/CommentController/ MotionCoauthorController, container+groupManager for EngagementController, and had spurious groupManager on AnalyticsController --- appinfo/info.xml | 1 - lib/AppInfo/Application.php | 21 ++++++++++++--------- lib/Controller/OriController.php | 8 ++++++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 9688c132..c7ffa078 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -68,7 +68,6 @@ Vrij en open source onder de EUPL-1.2-licentie. - OCA\Decidesk\BackgroundJob\MailReplyHandler OCA\Decidesk\BackgroundJob\OverdueActionItemsJob diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index beb9ca3b..e1ecd56d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -22,7 +22,6 @@ namespace OCA\Decidesk\AppInfo; -use OCA\Decidesk\BackgroundJob\MailReplyHandler; use OCA\Decidesk\BackgroundJob\OverdueActionItemsJob; use OCA\Decidesk\Mcp\DecideskToolProvider; use OCA\Decidesk\Controller\AnalyticsController; @@ -58,6 +57,7 @@ use OCA\Decidesk\Service\MotionService; use OCA\Decidesk\Service\NotificationPreferenceService; use OCA\Decidesk\Service\OriPublicationService; +use OCA\Decidesk\Service\ParticipantResolver; use OCA\Decidesk\Service\TaskService; use OCA\Decidesk\Service\VotingBehaviourService; use OCA\Decidesk\Service\VotingService; @@ -68,7 +68,6 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; -use OCP\BackgroundJob\IJobList; /** * Main application class for the Decidesk Nextcloud app. @@ -145,6 +144,7 @@ static function ($c): MinutesController { userSession: $c->get(\OCP\IUserSession::class), groupManager: $c->get(\OCP\IGroupManager::class), objectService: $c->get(ObjectService::class), + participantResolver: $c->get(ParticipantResolver::class), ); } ); @@ -238,7 +238,6 @@ static function ($c): AnalyticsController { request: $c->get(\OCP\IRequest::class), analyticsService: $c->get(ActionItemAnalyticsService::class), userSession: $c->get(\OCP\IUserSession::class), - groupManager: $c->get(\OCP\IGroupManager::class), ); } ); @@ -253,6 +252,7 @@ static function ($c): VotingService { logger: $c->get(\Psr\Log\LoggerInterface::class), oriPublicationService: $c->get(OriPublicationService::class), motionService: $c->get(MotionService::class), + participantResolver: $c->get(ParticipantResolver::class), ); } ); @@ -308,6 +308,7 @@ static function ($c): LiveMeetingController { liveDecisionService: $c->get(LiveDecisionService::class), userSession: $c->get(\OCP\IUserSession::class), groupManager: $c->get(\OCP\IGroupManager::class), + participantResolver: $c->get(ParticipantResolver::class), ); } ); @@ -480,6 +481,7 @@ static function ($c): TaskController { request: $c->get(\OCP\IRequest::class), taskService: $c->get(TaskService::class), userSession: $c->get(\OCP\IUserSession::class), + groupManager: $c->get(\OCP\IGroupManager::class), ); } ); @@ -502,6 +504,7 @@ static function ($c): CommentController { request: $c->get(\OCP\IRequest::class), commentService: $c->get(CommentService::class), userSession: $c->get(\OCP\IUserSession::class), + groupManager: $c->get(\OCP\IGroupManager::class), ); } ); @@ -546,6 +549,8 @@ static function ($c): EngagementController { request: $c->get(\OCP\IRequest::class), engagementService: $c->get(EngagementService::class), userSession: $c->get(\OCP\IUserSession::class), + groupManager: $c->get(\OCP\IGroupManager::class), + container: $c->get(\Psr\Container\ContainerInterface::class), ); } ); @@ -557,6 +562,7 @@ static function ($c): MotionCoauthorController { request: $c->get(\OCP\IRequest::class), coauthorService: $c->get(MotionCoauthorService::class), userSession: $c->get(\OCP\IUserSession::class), + groupManager: $c->get(\OCP\IGroupManager::class), ); } ); @@ -586,11 +592,8 @@ static function ($c): MotionCoauthorController { */ public function boot(IBootContext $context): void { - $serverContainer = $context->getServerContainer(); - $jobList = $serverContainer->get(IJobList::class); - if ($jobList->has(MailReplyHandler::class, null) === false) { - $jobList->add(MailReplyHandler::class); - } - + // C2: email-voting is disabled — MailReplyHandler is not registered. + // The background job remains in place for future re-enablement but must + // not be scheduled until the feature is audited and enabled deliberately. }//end boot() }//end class diff --git a/lib/Controller/OriController.php b/lib/Controller/OriController.php index fb3e7489..1564745d 100644 --- a/lib/Controller/OriController.php +++ b/lib/Controller/OriController.php @@ -283,7 +283,9 @@ public function preflightItem(string $resource, string $id): JSONResponse private function serializeOri(string $type, array $object): array { $self = ($object['@self'] ?? []); - $id = ($self['id'] ?? ($object['id'] ?? ($object['uuid'] ?? null))); + // L1: prefer the entity's own uuid field over @self.id (which is an + // internal OpenRegister reference and must not be surfaced publicly). + $id = ($object['uuid'] ?? ($object['id'] ?? ($self['id'] ?? null))); $payload = [ '@context' => self::ORI_CONTEXT, @@ -325,7 +327,9 @@ private function serializeOri(string $type, array $object): array $payload['text'] = $object['text']; } - if (isset($object['email']) === true) { + // C5: only expose email for Organisation-typed resources to prevent + // accidental leakage of private contact details from other types. + if ($type === 'Organization' && isset($object['email']) === true) { $payload['email'] = $object['email']; }