diff --git a/config/config.sample.php b/config/config.sample.php index a91df0ad5d049..77e970733e083 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -2697,6 +2697,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 2393fb82eb394..87c6d6d49cf25 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, ) { } @@ -73,6 +75,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 62ca08e786073..e05bdb239df13 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; @@ -68,6 +69,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 c1c7e587fd256..81c91e0efa8fa 100644 --- a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php @@ -18,7 +18,9 @@ use OCP\ISession; use OCP\IUser; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController; use Test\TestCase; @@ -44,6 +46,7 @@ class PasswordConfirmationMiddlewareTest extends TestCase { private IRequest $request; /** @var Manager&\PHPUnit\Framework\MockObject\MockObject */ private Manager $userManager; + private IRemoteAddress&MockObject $remoteAddress; #[\Override] protected function setUp(): void { @@ -60,6 +63,7 @@ protected function setUp(): void { 'test', $this->createMock(IRequest::class) ); + $this->remoteAddress = $this->createMock(IRemoteAddress::class); $this->middleware = new PasswordConfirmationMiddleware( $this->reflector, @@ -70,6 +74,7 @@ protected function setUp(): void { $this->logger, $this->request, $this->userManager, + $this->remoteAddress, ); }