From 6dd7a0a9665dab04966b4adddf0574ba0a3a7e9b Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 4 Aug 2026 15:57:24 +0200 Subject: [PATCH] feat: Add setting to bypass password confirmation on a selected ip ranges Signed-off-by: Carl Schwan --- config/config.sample.php | 13 +++++++ .../PasswordConfirmationMiddleware.php | 6 ++++ lib/private/Security/Ip/RemoteAddress.php | 27 ++++++++++++++ lib/public/Security/Ip/IRemoteAddress.php | 6 ++++ .../PasswordConfirmationMiddlewareTest.php | 36 +++++++++---------- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index 431efaa25d5ad..6099bbb8b94e6 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -2538,6 +2538,19 @@ */ 'allowed_admin_ranges' => ['192.0.2.42/32', '233.252.0.0/24', '2001:db8::13:37/64'], + /** + * List of trusted IP ranges that can bypass password confirmation. + * If non-empty, all endpoints marked with the PasswordConfirmationRequired attribute + * won't need a password confirmation when originating from IPs within these ranges. + * + * Supported formats: + * - IPv4 addresses or ranges, e.g., ``192.0.2.42/32``, ``233.252.0.0/24`` + * - IPv6 addresses or ranges, e.g., ``2001:db8::13:37/64`` + * + * Defaults to ``[]`` (empty array) + */ + 'allowed_no_password_confirmation_ranges' => ['192.0.2.42/32', '233.252.0.0/24', '2001:db8::13:37/64'], + /** * Maximum file size (in megabytes) for animating GIFs on public sharing pages. * If a GIF exceeds this size, a static preview is shown. diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index 5e8c7705fd591..316ed84ea3f84 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -21,6 +21,7 @@ use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\User\Backend\IPasswordConfirmationBackend; use Psr\Log\LoggerInterface; @@ -39,6 +40,7 @@ public function __construct( private readonly LoggerInterface $logger, private readonly IRequest $request, private readonly Manager $userManager, + private readonly IRemoteAddress $remoteAddress, ) { } @@ -72,6 +74,10 @@ public function beforeController(Controller $controller, string $methodName) { return; } } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { + if ($this->remoteAddress->allowsBypassPasswordConfirmation()) { + return; + } + // No scope to test } diff --git a/lib/private/Security/Ip/RemoteAddress.php b/lib/private/Security/Ip/RemoteAddress.php index 4eef881389855..338aca60eae45 100644 --- a/lib/private/Security/Ip/RemoteAddress.php +++ b/lib/private/Security/Ip/RemoteAddress.php @@ -17,6 +17,7 @@ class RemoteAddress implements IRemoteAddress, IAddress { public const SETTING_NAME = 'allowed_admin_ranges'; + public const SETTING_PASSWORD_CONFIRMATION_NAME = 'allowed_no_password_confirmation_ranges'; private readonly ?IAddress $ip; @@ -65,6 +66,32 @@ public function allowsAdminActions(): bool { return false; } + #[\Override] + public function allowsBypassPasswordConfirmation(): bool { + if ($this->ip === null) { + return false; + } + + $allowedAdminRanges = $this->config->getSystemValue(self::SETTING_PASSWORD_CONFIRMATION_NAME, false); + + // Apply restrictions on empty or invalid configuration + if ( + $allowedAdminRanges === false + || !is_array($allowedAdminRanges) + || empty($allowedAdminRanges) + ) { + return false; + } + + foreach ($allowedAdminRanges as $allowedAdminRange) { + if ((new Range($allowedAdminRange))->contains($this->ip)) { + return true; + } + } + + return false; + } + public function __toString(): string { return (string)$this->ip; } diff --git a/lib/public/Security/Ip/IRemoteAddress.php b/lib/public/Security/Ip/IRemoteAddress.php index 19a1dab973416..45c636dfba4ad 100644 --- a/lib/public/Security/Ip/IRemoteAddress.php +++ b/lib/public/Security/Ip/IRemoteAddress.php @@ -19,4 +19,10 @@ interface IRemoteAddress { * @since 30.0.0 */ public function allowsAdminActions(): bool; + + /** + * Check if the current remote address is allowed to bypass the password confirmation. + * @since 35.0.0 + */ + public function allowsBypassPasswordConfirmation(): bool; } diff --git a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php index e0d1c254c9dbf..4eca6beb08b07 100644 --- a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php @@ -18,31 +18,25 @@ use OCP\ISession; use OCP\IUser; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController; use Test\TestCase; class PasswordConfirmationMiddlewareTest extends TestCase { - /** @var ControllerMethodReflector */ - private $reflector; - /** @var ISession&\PHPUnit\Framework\MockObject\MockObject */ - private $session; - /** @var IUserSession&\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - /** @var IUser&\PHPUnit\Framework\MockObject\MockObject */ - private $user; - /** @var PasswordConfirmationMiddleware */ - private $middleware; - /** @var PasswordConfirmationMiddlewareController */ - private $controller; - /** @var ITimeFactory&\PHPUnit\Framework\MockObject\MockObject */ - private $timeFactory; - private IProvider&\PHPUnit\Framework\MockObject\MockObject $tokenProvider; - private LoggerInterface $logger; - /** @var IRequest&\PHPUnit\Framework\MockObject\MockObject */ - private IRequest $request; - /** @var Manager&\PHPUnit\Framework\MockObject\MockObject */ - private Manager $userManager; + private ControllerMethodReflector $reflector; + private ISession&MockObject $session; + private IUserSession&MockObject $userSession; + private IUser&MockObject $user; + private PasswordConfirmationMiddleware $middleware; + private PasswordConfirmationMiddlewareController $controller; + private ITimeFactory&MockObject $timeFactory; + private IProvider&MockObject $tokenProvider; + private LoggerInterface&MockObject $logger; + private IRequest&MockObject $request; + private Manager&MockObject $userManager; + private IRemoteAddress&MockObject $remoteAddress; protected function setUp(): void { $this->reflector = new ControllerMethodReflector(\OCP\Server::get(LoggerInterface::class)); @@ -58,6 +52,7 @@ protected function setUp(): void { 'test', $this->createMock(IRequest::class) ); + $this->remoteAddress = $this->createMock(IRemoteAddress::class); $this->middleware = new PasswordConfirmationMiddleware( $this->reflector, @@ -68,6 +63,7 @@ protected function setUp(): void { $this->logger, $this->request, $this->userManager, + $this->remoteAddress, ); }