2014-05-08 05:47:18 -04:00
< ? php
2024-05-23 03:26:56 -04:00
2014-05-08 05:47:18 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2014-05-08 05:47:18 -04:00
*/
namespace OC\AppFramework\Middleware\Security ;
2015-11-28 05:06:46 -05:00
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException ;
2014-05-11 11:55:59 -04:00
use OC\AppFramework\Utility\ControllerMethodReflector ;
2016-06-17 05:01:35 -04:00
use OC\Authentication\Exceptions\PasswordLoginForbiddenException ;
2016-05-25 03:58:01 -04:00
use OC\User\Session ;
2015-07-20 06:54:22 -04:00
use OCP\AppFramework\Controller ;
use OCP\AppFramework\Http ;
2023-04-24 11:13:18 -04:00
use OCP\AppFramework\Http\Attribute\CORS ;
use OCP\AppFramework\Http\Attribute\PublicPage ;
2015-07-20 06:54:22 -04:00
use OCP\AppFramework\Http\JSONResponse ;
2014-05-08 05:47:18 -04:00
use OCP\AppFramework\Http\Response ;
use OCP\AppFramework\Middleware ;
2016-05-25 03:58:01 -04:00
use OCP\IRequest ;
2023-10-06 06:46:37 -04:00
use OCP\ISession ;
2023-08-28 09:50:45 -04:00
use OCP\Security\Bruteforce\IThrottler ;
2024-07-15 09:25:45 -04:00
use Psr\Log\LoggerInterface ;
2023-04-24 11:13:18 -04:00
use ReflectionMethod ;
2014-05-08 05:47:18 -04:00
/**
2015-05-22 07:17:27 -04:00
* This middleware sets the correct CORS headers on a response if the
2014-05-08 05:47:18 -04:00
* controller has the @ CORS annotation . This is needed for webapps that want
2016-04-07 13:51:27 -04:00
* to access an API and don ' t run on the same domain , see
2014-05-08 05:47:18 -04:00
* https :// developer . mozilla . org / en - US / docs / Web / HTTP / Access_control_CORS
*/
class CORSMiddleware extends Middleware {
2016-07-20 12:36:15 -04:00
/** @var IRequest */
2014-05-08 05:47:18 -04:00
private $request ;
2016-07-20 12:36:15 -04:00
/** @var ControllerMethodReflector */
2014-05-11 11:55:59 -04:00
private $reflector ;
2016-07-20 12:36:15 -04:00
/** @var Session */
2015-05-22 07:17:27 -04:00
private $session ;
2023-08-28 09:50:45 -04:00
/** @var IThrottler */
2016-07-20 12:36:15 -04:00
private $throttler ;
2015-05-22 07:17:27 -04:00
public function __construct (
IRequest $request ,
2015-07-20 06:54:22 -04:00
ControllerMethodReflector $reflector ,
2016-07-20 12:36:15 -04:00
Session $session ,
2024-07-15 09:25:45 -04:00
IThrottler $throttler ,
private readonly LoggerInterface $logger ,
) {
2014-05-08 05:47:18 -04:00
$this -> request = $request ;
2014-05-11 11:55:59 -04:00
$this -> reflector = $reflector ;
2015-05-22 07:17:27 -04:00
$this -> session = $session ;
2016-07-20 12:36:15 -04:00
$this -> throttler = $throttler ;
2014-05-08 05:47:18 -04:00
}
2015-05-22 07:17:27 -04:00
/**
* This is being run in normal order before the controller is being
* called which allows several modifications and checks
*
* @ param Controller $controller the controller that is being called
* @ param string $methodName the name of the method that will be called on
* the controller
2015-07-20 06:54:22 -04:00
* @ throws SecurityException
2015-05-22 07:17:27 -04:00
* @ since 6.0 . 0
*/
2020-04-09 07:53:40 -04:00
public function beforeController ( $controller , $methodName ) {
2023-04-24 11:13:18 -04:00
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
2015-05-22 07:17:27 -04:00
// ensure that @CORS annotated API routes are not used in conjunction
// with session authentication since this enables CSRF attack vectors
2023-04-24 11:13:18 -04:00
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'CORS' , CORS :: class ) &&
( ! $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'PublicPage' , PublicPage :: class ) || $this -> session -> isLoggedIn ())) {
2021-05-01 09:48:35 -04:00
$user = array_key_exists ( 'PHP_AUTH_USER' , $this -> request -> server ) ? $this -> request -> server [ 'PHP_AUTH_USER' ] : null ;
$pass = array_key_exists ( 'PHP_AUTH_PW' , $this -> request -> server ) ? $this -> request -> server [ 'PHP_AUTH_PW' ] : null ;
2015-05-22 07:17:27 -04:00
2022-04-02 12:04:41 -04:00
// Allow to use the current session if a CSRF token is provided
if ( $this -> request -> passesCSRFCheck ()) {
return ;
}
2023-10-02 04:08:21 -04:00
// Skip CORS check for requests with AppAPI auth.
2023-10-06 06:46:37 -04:00
if ( $this -> session -> getSession () instanceof ISession && $this -> session -> getSession () -> get ( 'app_api' ) === true ) {
2023-10-02 04:08:21 -04:00
return ;
}
2015-05-22 07:17:27 -04:00
$this -> session -> logout ();
2016-06-17 05:01:35 -04:00
try {
2021-05-01 09:48:35 -04:00
if ( $user === null || $pass === null || ! $this -> session -> logClientIn ( $user , $pass , $this -> request , $this -> throttler )) {
2016-06-17 05:01:35 -04:00
throw new SecurityException ( 'CORS requires basic auth' , Http :: STATUS_UNAUTHORIZED );
}
} catch ( PasswordLoginForbiddenException $ex ) {
throw new SecurityException ( 'Password login forbidden, use token instead' , Http :: STATUS_UNAUTHORIZED );
2015-05-22 07:17:27 -04:00
}
}
}
2014-05-08 05:47:18 -04:00
/**
2023-04-24 11:13:18 -04:00
* @ template T
*
* @ param ReflectionMethod $reflectionMethod
* @ param string $annotationName
* @ param class - string < T > $attributeClass
* @ return boolean
*/
protected function hasAnnotationOrAttribute ( ReflectionMethod $reflectionMethod , string $annotationName , string $attributeClass ) : bool {
if ( $this -> reflector -> hasAnnotation ( $annotationName )) {
2024-11-12 16:15:08 -05:00
$this -> logger -> debug ( $reflectionMethod -> getDeclaringClass () -> getName () . '::' . $reflectionMethod -> getName () . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead' );
2023-04-24 11:13:18 -04:00
return true ;
}
if ( ! empty ( $reflectionMethod -> getAttributes ( $attributeClass ))) {
return true ;
}
return false ;
}
/**
* This is being run after a successful controller method call and allows
2014-05-08 05:47:18 -04:00
* the manipulation of a Response object . The middleware is run in reverse order
*
* @ 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 Response $response the generated response from the controller
* @ return Response a Response object
2015-07-20 06:54:22 -04:00
* @ throws SecurityException
2014-05-08 05:47:18 -04:00
*/
2020-04-09 07:53:40 -04:00
public function afterController ( $controller , $methodName , Response $response ) {
2023-04-24 11:13:18 -04:00
// only react if it's a CORS request and if the request sends origin and
2014-05-08 05:47:18 -04:00
2023-04-24 11:13:18 -04:00
if ( isset ( $this -> request -> server [ 'HTTP_ORIGIN' ])) {
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
if ( $this -> hasAnnotationOrAttribute ( $reflectionMethod , 'CORS' , CORS :: class )) {
// allow credentials headers must not be true or CSRF is possible
// otherwise
foreach ( $response -> getHeaders () as $header => $value ) {
if ( strtolower ( $header ) === 'access-control-allow-credentials' &&
strtolower ( trim ( $value )) === 'true' ) {
$msg = 'Access-Control-Allow-Credentials must not be ' .
'set to true in order to prevent CSRF' ;
throw new SecurityException ( $msg );
}
2014-05-08 05:47:18 -04:00
}
2023-04-24 11:13:18 -04:00
$origin = $this -> request -> server [ 'HTTP_ORIGIN' ];
$response -> addHeader ( 'Access-Control-Allow-Origin' , $origin );
}
2014-05-08 05:47:18 -04:00
}
return $response ;
}
2015-07-20 06:54:22 -04:00
/**
* If an SecurityException is being caught return a JSON error response
*
* @ 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
2016-04-07 13:51:27 -04:00
* @ throws \Exception the passed in exception if it can ' t handle it
2015-07-20 06:54:22 -04:00
* @ return Response a Response object or null in case that the exception could not be handled
*/
2020-04-09 07:53:40 -04:00
public function afterException ( $controller , $methodName , \Exception $exception ) {
2015-07-20 06:54:22 -04:00
if ( $exception instanceof SecurityException ) {
2020-10-05 09:12:57 -04:00
$response = new JSONResponse ([ 'message' => $exception -> getMessage ()]);
2015-07-20 06:54:22 -04:00
if ( $exception -> getCode () !== 0 ) {
$response -> setStatus ( $exception -> getCode ());
} else {
$response -> setStatus ( Http :: STATUS_INTERNAL_SERVER_ERROR );
}
return $response ;
}
throw $exception ;
}
2014-05-08 05:47:18 -04:00
}