Completed
Pull Request — master (#1917)
by Lukas
40:33 queued 30:26
created

SecurityMiddleware::afterController()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 6
nop 3
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bernhard Posselt <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Morris Jobke <[email protected]>
8
 * @author Roeland Jago Douma <[email protected]>
9
 * @author Stefan Weil <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Thomas Tanghus <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
30
namespace OC\AppFramework\Middleware\Security;
31
32
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
33
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
34
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
35
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
36
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
37
use OC\AppFramework\Utility\ControllerMethodReflector;
38
use OC\Security\CSP\ContentSecurityPolicyManager;
39
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
40
use OC\Security\CSRF\CsrfTokenManager;
41
use OCP\AppFramework\Http\ContentSecurityPolicy;
42
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
43
use OCP\AppFramework\Http\RedirectResponse;
44
use OCP\AppFramework\Http\TemplateResponse;
45
use OCP\AppFramework\Middleware;
46
use OCP\AppFramework\Http\Response;
47
use OCP\AppFramework\Http\JSONResponse;
48
use OCP\AppFramework\OCSController;
49
use OCP\INavigationManager;
50
use OCP\IURLGenerator;
51
use OCP\IRequest;
52
use OCP\ILogger;
53
use OCP\AppFramework\Controller;
54
use OCP\Util;
55
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
56
57
/**
58
 * Used to do all the authentication and checking stuff for a controller method
59
 * It reads out the annotations of a controller method and checks which if
60
 * security things should be checked and also handles errors in case a security
61
 * check fails
62
 */
63
class SecurityMiddleware extends Middleware {
64
	/** @var INavigationManager */
65
	private $navigationManager;
66
	/** @var IRequest */
67
	private $request;
68
	/** @var ControllerMethodReflector */
69
	private $reflector;
70
	/** @var string */
71
	private $appName;
72
	/** @var IURLGenerator */
73
	private $urlGenerator;
74
	/** @var ILogger */
75
	private $logger;
76
	/** @var bool */
77
	private $isLoggedIn;
78
	/** @var bool */
79
	private $isAdminUser;
80
	/** @var ContentSecurityPolicyManager */
81
	private $contentSecurityPolicyManager;
82
	/** @var CsrfTokenManager */
83
	private $csrfTokenManager;
84
	/** @var ContentSecurityPolicyNonceManager */
85
	private $cspNonceManager;
86
87
	/**
88
	 * @param IRequest $request
89
	 * @param ControllerMethodReflector $reflector
90
	 * @param INavigationManager $navigationManager
91
	 * @param IURLGenerator $urlGenerator
92
	 * @param ILogger $logger
93
	 * @param string $appName
94
	 * @param bool $isLoggedIn
95
	 * @param bool $isAdminUser
96
	 * @param ContentSecurityPolicyManager $contentSecurityPolicyManager
97
	 * @param CSRFTokenManager $csrfTokenManager
98
	 * @param ContentSecurityPolicyNonceManager $cspNonceManager
99
	 */
100
	public function __construct(IRequest $request,
101
								ControllerMethodReflector $reflector,
102
								INavigationManager $navigationManager,
103
								IURLGenerator $urlGenerator,
104
								ILogger $logger,
105
								$appName,
106
								$isLoggedIn,
107
								$isAdminUser,
108
								ContentSecurityPolicyManager $contentSecurityPolicyManager,
109
								CsrfTokenManager $csrfTokenManager,
110
								ContentSecurityPolicyNonceManager $cspNonceManager) {
111
		$this->navigationManager = $navigationManager;
112
		$this->request = $request;
113
		$this->reflector = $reflector;
114
		$this->appName = $appName;
115
		$this->urlGenerator = $urlGenerator;
116
		$this->logger = $logger;
117
		$this->isLoggedIn = $isLoggedIn;
118
		$this->isAdminUser = $isAdminUser;
119
		$this->contentSecurityPolicyManager = $contentSecurityPolicyManager;
120
		$this->csrfTokenManager = $csrfTokenManager;
121
		$this->cspNonceManager = $cspNonceManager;
122
	}
123
124
125
	/**
126
	 * This runs all the security checks before a method call. The
127
	 * security checks are determined by inspecting the controller method
128
	 * annotations
129
	 * @param Controller $controller the controller
130
	 * @param string $methodName the name of the method
131
	 * @throws SecurityException when a security check fails
132
	 */
133
	public function beforeController($controller, $methodName) {
134
135
		// this will set the current navigation entry of the app, use this only
136
		// for normal HTML requests and not for AJAX requests
137
		$this->navigationManager->setActiveEntry($this->appName);
138
139
		// security checks
140
		$isPublicPage = $this->reflector->hasAnnotation('PublicPage');
141
		if(!$isPublicPage) {
142
			if(!$this->isLoggedIn) {
143
				throw new NotLoggedInException();
144
			}
145
146
			if(!$this->reflector->hasAnnotation('NoAdminRequired')) {
147
				if(!$this->isAdminUser) {
148
					throw new NotAdminException();
149
				}
150
			}
151
		}
152
153
		// Check for strict cookie requirement
154
		if($this->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
155
			if(!$this->request->passesStrictCookieCheck()) {
156
				throw new StrictCookieMissingException();
157
			}
158
		}
159
		// CSRF check - also registers the CSRF token since the session may be closed later
160
		Util::callRegister();
161
		if(!$this->reflector->hasAnnotation('NoCSRFRequired')) {
162
			/*
163
			 * Only allow the CSRF check to fail on OCS Requests. This kind of
164
			 * hacks around that we have no full token auth in place yet and we
165
			 * do want to offer CSRF checks for web requests.
166
			 */
167
			if(!$this->request->passesCSRFCheck() && !(
168
					$controller instanceof OCSController &&
169
					$this->request->getHeader('OCS-APIREQUEST') === 'true')) {
170
				throw new CrossSiteRequestForgeryException();
171
			}
172
		}
173
174
		/**
175
		 * FIXME: Use DI once available
176
		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
177
		 * The getAppPath() check is here since components such as settings also use the AppFramework and
178
		 * therefore won't pass this check.
179
		 */
180
		if(\OC_App::getAppPath($this->appName) !== false && !\OC_App::isEnabled($this->appName)) {
181
			throw new AppNotEnabledException();
182
		}
183
184
	}
185
186
	/**
187
	 * Performs the default CSP modifications that may be injected by other
188
	 * applications
189
	 *
190
	 * @param Controller $controller
191
	 * @param string $methodName
192
	 * @param Response $response
193
	 * @return Response
194
	 */
195
	public function afterController($controller, $methodName, Response $response) {
196
		$policy = !is_null($response->getContentSecurityPolicy()) ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy();
197
198
		if (get_class($policy) === EmptyContentSecurityPolicy::class) {
199
			return $response;
200
		}
201
202
		$defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
203
		$defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);
204
205
		if($this->cspNonceManager->browserSupportsCspV3()) {
206
			$defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue());
207
		}
208
209
		$response->setContentSecurityPolicy($defaultPolicy);
210
211
		return $response;
212
	}
213
214
	/**
215
	 * If an SecurityException is being caught, ajax requests return a JSON error
216
	 * response and non ajax requests redirect to the index
217
	 * @param Controller $controller the controller that is being called
218
	 * @param string $methodName the name of the method that will be called on
219
	 *                           the controller
220
	 * @param \Exception $exception the thrown exception
221
	 * @throws \Exception the passed in exception if it can't handle it
222
	 * @return Response a Response object or null in case that the exception could not be handled
223
	 */
224
	public function afterException($controller, $methodName, \Exception $exception) {
225
		if($exception instanceof SecurityException) {
226
			if($exception instanceof StrictCookieMissingException) {
227
				return new RedirectResponse(\OC::$WEBROOT);
228
 			}
229
			if (stripos($this->request->getHeader('Accept'),'html') === false) {
230
				$response = new JSONResponse(
231
					array('message' => $exception->getMessage()),
232
					$exception->getCode()
233
				);
234
			} else {
235
				if($exception instanceof NotLoggedInException) {
236
					$url = $this->urlGenerator->linkToRoute(
237
						'core.login.showLoginForm',
238
						[
239
							'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...
240
						]
241
					);
242
					$response = new RedirectResponse($url);
243
				} else {
244
					$response = new TemplateResponse('core', '403', ['file' => $exception->getMessage()], 'guest');
245
					$response->setStatus($exception->getCode());
246
				}
247
			}
248
249
			$this->logger->debug($exception->getMessage());
250
			return $response;
251
		}
252
253
		throw $exception;
254
	}
255
256
}
257