owncloud/core

View on GitHub
lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/**
 * @author Bernhard Posselt <dev@bernhard-posselt.com>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Roeland Jago Douma <rullzer@owncloud.com>
 * @author Stefan Weil <sw@weilnetz.de>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Thomas Tanghus <thomas@tanghus.net>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\AppFramework\Middleware\Security;

use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Core\Controller\LoginController;
use OC\OCS\Result;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OC\User\LoginException;
use OCP\API;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCSController;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IRequest;
use OCP\ILogger;
use OCP\AppFramework\Controller;
use OCP\IUserSession;
use OCP\Util;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;

/**
 * Used to do all the authentication and checking stuff for a controller method
 * It reads out the annotations of a controller method and checks which if
 * security things should be checked and also handles errors in case a security
 * check fails
 */
class SecurityMiddleware extends Middleware {
    /** @var INavigationManager */
    private $navigationManager;
    /** @var IRequest */
    private $request;
    /** @var ControllerMethodReflector */
    private $reflector;
    /** @var string */
    private $appName;
    /** @var IURLGenerator */
    private $urlGenerator;
    /** @var ILogger */
    private $logger;
    /** @var bool */
    private $isAdminUser;
    /** @var ContentSecurityPolicyManager */
    private $contentSecurityPolicyManager;
    /** @var IUserSession */
    private $session;

    /**
     * @param IRequest $request
     * @param ControllerMethodReflector $reflector
     * @param INavigationManager $navigationManager
     * @param IURLGenerator $urlGenerator
     * @param ILogger $logger
     * @param IUserSession $session
     * @param string $appName
     * @param bool $isAdminUser
     * @param ContentSecurityPolicyManager $contentSecurityPolicyManager
     */
    public function __construct(
        IRequest $request,
        ControllerMethodReflector $reflector,
        INavigationManager $navigationManager,
        IURLGenerator $urlGenerator,
        ILogger $logger,
        IUserSession $session,
        $appName,
        $isAdminUser,
        ContentSecurityPolicyManager $contentSecurityPolicyManager
    ) {
        $this->navigationManager = $navigationManager;
        $this->request = $request;
        $this->reflector = $reflector;
        $this->appName = $appName;
        $this->urlGenerator = $urlGenerator;
        $this->logger = $logger;
        $this->session = $session;
        $this->isAdminUser = $isAdminUser;
        $this->contentSecurityPolicyManager = $contentSecurityPolicyManager;
    }

    /**
     * This runs all the security checks before a method call. The
     * security checks are determined by inspecting the controller method
     * annotations
     * @param string $controller the controllername or string
     * @param string $methodName the name of the method
     * @throws SecurityException when a security check fails
     */
    public function beforeController($controller, $methodName) {
        // this will set the current navigation entry of the app, use this only
        // for normal HTML requests and not for AJAX requests
        $this->navigationManager->setActiveEntry($this->appName);

        // security checks
        $isPublicPage = $this->reflector->hasAnnotation('PublicPage');
        if (!$isPublicPage) {
            if (!$this->session->isLoggedIn() && !\OC::handleLogin($this->request)) {
                throw new NotLoggedInException();
            }

            if (!$this->reflector->hasAnnotation('NoAdminRequired')) {
                if (!$this->isAdminUser) {
                    throw new NotAdminException();
                }
            }
        }

        // CSRF check - also registers the CSRF token since the session may be closed later
        Util::callRegister();
        if (!$this->reflector->hasAnnotation('NoCSRFRequired')) {
            $hasNoAuthHeader = ($this->request->getHeader("Authorization") === null || trim($this->request->getHeader("Authorization")) === '');
            if (!$this->request->passesCSRFCheck() && $hasNoAuthHeader) {
                throw new CrossSiteRequestForgeryException();
            }
        }

        /**
         * FIXME: Use DI once available
         * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
         * The getAppPath() check is here since components such as settings also use the AppFramework and
         * therefore won't pass this check.
         */
        if (\OC_App::getAppPath($this->appName) !== false && !\OC_App::isEnabled($this->appName)) {
            throw new AppNotEnabledException();
        }
    }

    /**
     * Performs the default CSP modifications that may be injected by other
     * applications
     *
     * @param Controller $controller
     * @param string $methodName
     * @param Response $response
     * @return Response
     */
    public function afterController($controller, $methodName, Response $response) {
        $policy = $response->getContentSecurityPolicy() !== null ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy();

        $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
        $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);

        $response->setContentSecurityPolicy($defaultPolicy);

        return $response;
    }

    /**
     * If an SecurityException is being caught, ajax requests return a JSON error
     * response and non ajax requests redirect to the index
     * @param Controller $controller the controller that is being called
     * @param string $methodName the name of the method that will be called on
     *                           the controller
     * @param \Exception $exception the thrown exception
     * @throws \Exception the passed in exception if it can't handle it
     * @return Response a Response object or null in case that the exception could not be handled
     */
    public function afterException($controller, $methodName, \Exception $exception) {
        $headers = [];
        if ($controller instanceof OCSController) {
            if ($this->request->getHeader('OC-RequestAppPassword') === 'true'
                    && ($exception instanceof NotLoggedInException || $exception instanceof LoginException)) {
                $headers['WWW-Authenticate'] = 'Basic realm="ownCloud App Authentication Requested"';
            }
            if ($exception instanceof NotLoggedInException) {
                return $controller->buildResponse(new Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised', $headers));
            }
            if ($exception instanceof LoginException) {
                return $controller->buildResponse(new Result(null, API::RESPOND_UNAUTHORISED, $exception->getMessage(), $headers));
            }
            if ($exception instanceof NotAdminException) {
                return $controller->buildResponse(new Result(null, $exception->getCode(), $exception->getMessage(), $headers));
            }
            return $controller->buildResponse(new Result(null, API::RESPOND_SERVER_ERROR, $exception->getMessage(), $headers));
        }

        if ($exception instanceof SecurityException) {
            $accept_header = $this->request->getHeader('Accept') ?? '';
            if ($accept_header !== '*/*'
                && \stripos($accept_header, 'html') === false
            ) {
                $response = new JSONResponse(
                    ['message' => $exception->getMessage()],
                    $exception->getCode()
                );
            } else {
                if ($exception instanceof NotLoggedInException) {
                    $url = $this->urlGenerator->linkToRoute(
                        'core.login.showLoginForm',
                        [
                            'redirect_url' => \urlencode($this->request->server['REQUEST_URI']),
                        ]
                    );
                    $response = new RedirectResponse($url);
                } elseif (
                    $methodName === 'tryLogin'
                    && $exception instanceof CrossSiteRequestForgeryException
                    && $controller instanceof LoginController
                ) {
                    $this->logger->debug($exception->getMessage());
                    $controller->getSession()->set(
                        'loginMessages',
                        [
                            ['csrf_error'],
                            []
                        ]
                    );
                    return $controller->showLoginForm(null, null, null);
                } else {
                    $response = new TemplateResponse('core', '403', ['file' => $exception->getMessage()], 'guest');
                    $response->setStatus($exception->getCode());
                }
            }

            $this->logger->debug($exception->getMessage());
            return $response;
        }

        throw $exception;
    }
}