1
0

SecurityMiddleware.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OC\AppFramework\Middleware\Security;
  9. use OC\AppFramework\Middleware\Security\Exceptions\AdminIpNotAllowedException;
  10. use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
  11. use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
  12. use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException;
  13. use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
  14. use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
  15. use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
  16. use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
  17. use OC\AppFramework\Utility\ControllerMethodReflector;
  18. use OC\Settings\AuthorizedGroupMapper;
  19. use OC\User\Session;
  20. use OCP\App\AppPathNotFoundException;
  21. use OCP\App\IAppManager;
  22. use OCP\AppFramework\Controller;
  23. use OCP\AppFramework\Http\Attribute\AppApiAdminAccessWithoutUser;
  24. use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
  25. use OCP\AppFramework\Http\Attribute\ExAppRequired;
  26. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  27. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  28. use OCP\AppFramework\Http\Attribute\PublicPage;
  29. use OCP\AppFramework\Http\Attribute\StrictCookiesRequired;
  30. use OCP\AppFramework\Http\Attribute\SubAdminRequired;
  31. use OCP\AppFramework\Http\JSONResponse;
  32. use OCP\AppFramework\Http\RedirectResponse;
  33. use OCP\AppFramework\Http\Response;
  34. use OCP\AppFramework\Http\TemplateResponse;
  35. use OCP\AppFramework\Middleware;
  36. use OCP\AppFramework\OCSController;
  37. use OCP\IL10N;
  38. use OCP\INavigationManager;
  39. use OCP\IRequest;
  40. use OCP\IURLGenerator;
  41. use OCP\IUserSession;
  42. use OCP\Security\Ip\IRemoteAddress;
  43. use OCP\Util;
  44. use Psr\Log\LoggerInterface;
  45. use ReflectionMethod;
  46. /**
  47. * Used to do all the authentication and checking stuff for a controller method
  48. * It reads out the annotations of a controller method and checks which if
  49. * security things should be checked and also handles errors in case a security
  50. * check fails
  51. */
  52. class SecurityMiddleware extends Middleware {
  53. public function __construct(
  54. private IRequest $request,
  55. private ControllerMethodReflector $reflector,
  56. private INavigationManager $navigationManager,
  57. private IURLGenerator $urlGenerator,
  58. private LoggerInterface $logger,
  59. private string $appName,
  60. private bool $isLoggedIn,
  61. private bool $isAdminUser,
  62. private bool $isSubAdmin,
  63. private IAppManager $appManager,
  64. private IL10N $l10n,
  65. private AuthorizedGroupMapper $groupAuthorizationMapper,
  66. private IUserSession $userSession,
  67. private IRemoteAddress $remoteAddress,
  68. ) {
  69. }
  70. /**
  71. * This runs all the security checks before a method call. The
  72. * security checks are determined by inspecting the controller method
  73. * annotations
  74. *
  75. * @param Controller $controller the controller
  76. * @param string $methodName the name of the method
  77. * @throws SecurityException when a security check fails
  78. *
  79. * @suppress PhanUndeclaredClassConstant
  80. */
  81. public function beforeController($controller, $methodName) {
  82. // this will set the current navigation entry of the app, use this only
  83. // for normal HTML requests and not for AJAX requests
  84. $this->navigationManager->setActiveEntry($this->appName);
  85. if (get_class($controller) === \OCA\Talk\Controller\PageController::class && $methodName === 'showCall') {
  86. $this->navigationManager->setActiveEntry('spreed');
  87. }
  88. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  89. // security checks
  90. $isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
  91. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'ExAppRequired', ExAppRequired::class)) {
  92. if (!$this->userSession instanceof Session || $this->userSession->getSession()->get('app_api') !== true) {
  93. throw new ExAppRequiredException();
  94. }
  95. } elseif (!$isPublicPage) {
  96. $authorized = false;
  97. if ($this->hasAnnotationOrAttribute($reflectionMethod, null, AppApiAdminAccessWithoutUser::class)) {
  98. // this attribute allows ExApp to access admin endpoints only if "userId" is "null"
  99. if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) {
  100. $authorized = true;
  101. }
  102. }
  103. if (!$authorized && !$this->isLoggedIn) {
  104. throw new NotLoggedInException();
  105. }
  106. if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
  107. $authorized = $this->isAdminUser;
  108. if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
  109. $authorized = $this->isSubAdmin;
  110. }
  111. if (!$authorized) {
  112. $settingClasses = $this->getAuthorizedAdminSettingClasses($reflectionMethod);
  113. $authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
  114. foreach ($settingClasses as $settingClass) {
  115. $authorized = in_array($settingClass, $authorizedClasses, true);
  116. if ($authorized) {
  117. break;
  118. }
  119. }
  120. }
  121. if (!$authorized) {
  122. throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting'));
  123. }
  124. if (!$this->remoteAddress->allowsAdminActions()) {
  125. throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
  126. }
  127. }
  128. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
  129. && !$this->isSubAdmin
  130. && !$this->isAdminUser
  131. && !$authorized) {
  132. throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin'));
  133. }
  134. if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
  135. && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
  136. && !$this->isAdminUser
  137. && !$authorized) {
  138. throw new NotAdminException($this->l10n->t('Logged in account must be an admin'));
  139. }
  140. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
  141. && !$this->remoteAddress->allowsAdminActions()) {
  142. throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
  143. }
  144. if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
  145. && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
  146. && !$this->remoteAddress->allowsAdminActions()) {
  147. throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
  148. }
  149. }
  150. // Check for strict cookie requirement
  151. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) ||
  152. !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
  153. if (!$this->request->passesStrictCookieCheck()) {
  154. throw new StrictCookieMissingException();
  155. }
  156. }
  157. // CSRF check - also registers the CSRF token since the session may be closed later
  158. Util::callRegister();
  159. if ($this->isInvalidCSRFRequired($reflectionMethod)) {
  160. /*
  161. * Only allow the CSRF check to fail on OCS Requests. This kind of
  162. * hacks around that we have no full token auth in place yet and we
  163. * do want to offer CSRF checks for web requests.
  164. *
  165. * Additionally we allow Bearer authenticated requests to pass on OCS routes.
  166. * This allows oauth apps (e.g. moodle) to use the OCS endpoints
  167. */
  168. if (!$controller instanceof OCSController || !$this->isValidOCSRequest()) {
  169. throw new CrossSiteRequestForgeryException();
  170. }
  171. }
  172. /**
  173. * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
  174. * The getAppPath() check is here since components such as settings also use the AppFramework and
  175. * therefore won't pass this check.
  176. * If page is public, app does not need to be enabled for current user/visitor
  177. */
  178. try {
  179. $appPath = $this->appManager->getAppPath($this->appName);
  180. } catch (AppPathNotFoundException $e) {
  181. $appPath = false;
  182. }
  183. if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
  184. throw new AppNotEnabledException();
  185. }
  186. }
  187. private function isInvalidCSRFRequired(ReflectionMethod $reflectionMethod): bool {
  188. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
  189. return false;
  190. }
  191. return !$this->request->passesCSRFCheck();
  192. }
  193. private function isValidOCSRequest(): bool {
  194. return $this->request->getHeader('OCS-APIREQUEST') === 'true'
  195. || str_starts_with($this->request->getHeader('Authorization'), 'Bearer ');
  196. }
  197. /**
  198. * @template T
  199. *
  200. * @param ReflectionMethod $reflectionMethod
  201. * @param ?string $annotationName
  202. * @param class-string<T> $attributeClass
  203. * @return boolean
  204. */
  205. protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, ?string $annotationName, string $attributeClass): bool {
  206. if (!empty($reflectionMethod->getAttributes($attributeClass))) {
  207. return true;
  208. }
  209. if ($annotationName && $this->reflector->hasAnnotation($annotationName)) {
  210. $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead');
  211. return true;
  212. }
  213. return false;
  214. }
  215. /**
  216. * @param ReflectionMethod $reflectionMethod
  217. * @return string[]
  218. */
  219. protected function getAuthorizedAdminSettingClasses(ReflectionMethod $reflectionMethod): array {
  220. $classes = [];
  221. if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
  222. $classes = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings'));
  223. }
  224. $attributes = $reflectionMethod->getAttributes(AuthorizedAdminSetting::class);
  225. if (!empty($attributes)) {
  226. foreach ($attributes as $attribute) {
  227. /** @var AuthorizedAdminSetting $setting */
  228. $setting = $attribute->newInstance();
  229. $classes[] = $setting->getSettings();
  230. }
  231. }
  232. return $classes;
  233. }
  234. /**
  235. * If an SecurityException is being caught, ajax requests return a JSON error
  236. * response and non ajax requests redirect to the index
  237. *
  238. * @param Controller $controller the controller that is being called
  239. * @param string $methodName the name of the method that will be called on
  240. * the controller
  241. * @param \Exception $exception the thrown exception
  242. * @return Response a Response object or null in case that the exception could not be handled
  243. * @throws \Exception the passed in exception if it can't handle it
  244. */
  245. public function afterException($controller, $methodName, \Exception $exception): Response {
  246. if ($exception instanceof SecurityException) {
  247. if ($exception instanceof StrictCookieMissingException) {
  248. return new RedirectResponse(\OC::$WEBROOT . '/');
  249. }
  250. if (stripos($this->request->getHeader('Accept'), 'html') === false) {
  251. $response = new JSONResponse(
  252. ['message' => $exception->getMessage()],
  253. $exception->getCode()
  254. );
  255. } else {
  256. if ($exception instanceof NotLoggedInException) {
  257. $params = [];
  258. if (isset($this->request->server['REQUEST_URI'])) {
  259. $params['redirect_url'] = $this->request->server['REQUEST_URI'];
  260. }
  261. $usernamePrefill = $this->request->getParam('user', '');
  262. if ($usernamePrefill !== '') {
  263. $params['user'] = $usernamePrefill;
  264. }
  265. if ($this->request->getParam('direct')) {
  266. $params['direct'] = 1;
  267. }
  268. $url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
  269. $response = new RedirectResponse($url);
  270. } else {
  271. $response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
  272. $response->setStatus($exception->getCode());
  273. }
  274. }
  275. $this->logger->debug($exception->getMessage(), [
  276. 'exception' => $exception,
  277. ]);
  278. return $response;
  279. }
  280. throw $exception;
  281. }
  282. }