Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

SecurityMiddleware::afterController()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Bernhard Posselt <[email protected]>
4
 * @author Lukas Reschke <[email protected]>
5
 * @author Morris Jobke <[email protected]>
6
 * @author Roeland Jago Douma <[email protected]>
7
 * @author Stefan Weil <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 * @author Thomas Tanghus <[email protected]>
10
 *
11
 * @copyright Copyright (c) 2018, ownCloud GmbH
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OC\AppFramework\Middleware\Security;
29
30
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
31
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
32
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
33
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
34
use OC\AppFramework\Utility\ControllerMethodReflector;
35
use OC\Core\Controller\LoginController;
36
use OC\Security\CSP\ContentSecurityPolicyManager;
37
use OCP\AppFramework\Http\ContentSecurityPolicy;
38
use OCP\AppFramework\Http\RedirectResponse;
39
use OCP\AppFramework\Http\TemplateResponse;
40
use OCP\AppFramework\Middleware;
41
use OCP\AppFramework\Http\Response;
42
use OCP\AppFramework\Http\JSONResponse;
43
use OCP\INavigationManager;
44
use OCP\IURLGenerator;
45
use OCP\IRequest;
46
use OCP\ILogger;
47
use OCP\AppFramework\Controller;
48
use OCP\IUserSession;
49
use OCP\Util;
50
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
51
52
/**
53
 * Used to do all the authentication and checking stuff for a controller method
54
 * It reads out the annotations of a controller method and checks which if
55
 * security things should be checked and also handles errors in case a security
56
 * check fails
57
 */
58
class SecurityMiddleware extends Middleware {
59
	/** @var INavigationManager */
60
	private $navigationManager;
61
	/** @var IRequest */
62
	private $request;
63
	/** @var ControllerMethodReflector */
64
	private $reflector;
65
	/** @var string */
66
	private $appName;
67
	/** @var IURLGenerator */
68
	private $urlGenerator;
69
	/** @var ILogger */
70
	private $logger;
71
	/** @var bool */
72
	private $isAdminUser;
73
	/** @var ContentSecurityPolicyManager */
74
	private $contentSecurityPolicyManager;
75
	/** @var IUserSession */
76
	private $session;
77
78
	/**
79
	 * @param IRequest $request
80
	 * @param ControllerMethodReflector $reflector
81
	 * @param INavigationManager $navigationManager
82
	 * @param IURLGenerator $urlGenerator
83
	 * @param ILogger $logger
84
	 * @param IUserSession $session
85
	 * @param string $appName
86
	 * @param bool $isAdminUser
87
	 * @param ContentSecurityPolicyManager $contentSecurityPolicyManager
88
	 */
89
	public function __construct(IRequest $request,
90
								ControllerMethodReflector $reflector,
91
								INavigationManager $navigationManager,
92
								IURLGenerator $urlGenerator,
93
								ILogger $logger,
94
								IUserSession $session,
95
								$appName,
96
								$isAdminUser,
97
								ContentSecurityPolicyManager $contentSecurityPolicyManager) {
98
		$this->navigationManager = $navigationManager;
99
		$this->request = $request;
100
		$this->reflector = $reflector;
101
		$this->appName = $appName;
102
		$this->urlGenerator = $urlGenerator;
103
		$this->logger = $logger;
104
		$this->session = $session;
105
		$this->isAdminUser = $isAdminUser;
106
		$this->contentSecurityPolicyManager = $contentSecurityPolicyManager;
107
	}
108
109
	/**
110
	 * This runs all the security checks before a method call. The
111
	 * security checks are determined by inspecting the controller method
112
	 * annotations
113
	 * @param string $controller the controllername or string
114
	 * @param string $methodName the name of the method
115
	 * @throws SecurityException when a security check fails
116
	 */
117
	public function beforeController($controller, $methodName) {
118
119
		// this will set the current navigation entry of the app, use this only
120
		// for normal HTML requests and not for AJAX requests
121
		$this->navigationManager->setActiveEntry($this->appName);
122
123
		// security checks
124
		$isPublicPage = $this->reflector->hasAnnotation('PublicPage');
125
		if (!$isPublicPage) {
126
			if (!$this->isLoggedIn()) {
127
				throw new NotLoggedInException();
128
			}
129
130
			if (!$this->reflector->hasAnnotation('NoAdminRequired')) {
131
				if (!$this->isAdminUser) {
132
					throw new NotAdminException();
133
				}
134
			}
135
		}
136
137
		// CSRF check - also registers the CSRF token since the session may be closed later
138
		Util::callRegister();
139
		if (!$this->reflector->hasAnnotation('NoCSRFRequired')) {
140
			if (!$this->request->passesCSRFCheck()) {
141
				throw new CrossSiteRequestForgeryException();
142
			}
143
		}
144
145
		/**
146
		 * FIXME: Use DI once available
147
		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
148
		 * The getAppPath() check is here since components such as settings also use the AppFramework and
149
		 * therefore won't pass this check.
150
		 */
151
		if (\OC_App::getAppPath($this->appName) !== false && !\OC_App::isEnabled($this->appName)) {
152
			throw new AppNotEnabledException();
153
		}
154
	}
155
156
	/**
157
	 * Performs the default CSP modifications that may be injected by other
158
	 * applications
159
	 *
160
	 * @param Controller $controller
161
	 * @param string $methodName
162
	 * @param Response $response
163
	 * @return Response
164
	 */
165
	public function afterController($controller, $methodName, Response $response) {
166
		$policy = $response->getContentSecurityPolicy() !== null ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy();
167
168
		$defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
169
		$defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);
170
171
		$response->setContentSecurityPolicy($defaultPolicy);
172
173
		return $response;
174
	}
175
176
	/**
177
	 * If an SecurityException is being caught, ajax requests return a JSON error
178
	 * response and non ajax requests redirect to the index
179
	 * @param Controller $controller the controller that is being called
180
	 * @param string $methodName the name of the method that will be called on
181
	 *                           the controller
182
	 * @param \Exception $exception the thrown exception
183
	 * @throws \Exception the passed in exception if it can't handle it
184
	 * @return Response a Response object or null in case that the exception could not be handled
185
	 */
186
	public function afterException($controller, $methodName, \Exception $exception) {
187
		if ($exception instanceof SecurityException) {
188
			if (\stripos($this->request->getHeader('Accept'), 'html') === false) {
189
				$response = new JSONResponse(
190
					['message' => $exception->getMessage()],
191
					$exception->getCode()
192
				);
193
			} else {
194
				if ($exception instanceof NotLoggedInException) {
195
					$url = $this->urlGenerator->linkToRoute(
196
						'core.login.showLoginForm',
197
						[
198
							'redirect_url' => \urlencode($this->request->server['REQUEST_URI']),
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
199
						]
200
					);
201
					$response = new RedirectResponse($url);
202
				} elseif (
203
					$methodName === 'tryLogin'
204
					&& $exception instanceof CrossSiteRequestForgeryException
205
					&& $controller instanceof LoginController
206
				) {
207
					$this->logger->debug($exception->getMessage());
208
					$controller->getSession()->set(
209
						'loginMessages',
210
						[
211
							['csrf_error'],
212
							[]
213
						]
214
					);
215
					return $controller->showLoginForm(null, null, null);
216
				} else {
217
					$response = new TemplateResponse('core', '403', ['file' => $exception->getMessage()], 'guest');
218
					$response->setStatus($exception->getCode());
219
				}
220
			}
221
222
			$this->logger->debug($exception->getMessage());
223
			return $response;
224
		}
225
226
		throw $exception;
227
	}
228
229
	private function isLoggedIn() {
230
		static $loginCalled = false;
231
		if (!$loginCalled && !$this->session->isLoggedIn()) {
232
			\OC::handleLogin($this->request);
233
			$loginCalled = true;
234
		}
235
		return $this->session->isLoggedIn();
236
	}
237
}
238