-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e62c4f9
commit a35305c
Showing
4 changed files
with
166 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
name: JSON:API Obscurity | ||
description: Obscures JSON:API path as recommended in security considerations. | ||
package: Web services | ||
type: module | ||
core_version_requirement: ^9 || ^10 | ||
dependencies: | ||
- drupal:jsonapi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* Obscures JSON:API path as recommended in security considerations. | ||
* | ||
* @see https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/security-considerations | ||
*/ | ||
|
||
/** | ||
* Implements hook_requirements(). | ||
*/ | ||
function jsonapi_obscurity_requirements(string $phase): array { | ||
|
||
if ($phase == 'runtime') { | ||
$requirements['jsonapi_obscurity'] = [ | ||
'title' => t('JSON:API Obscurity'), | ||
'value' => t('Obscurity prefix not defined!'), | ||
]; | ||
if (empty(\Drupal::getContainer()->getParameter('jsonapi_obscurity.prefix'))) { | ||
$requirements['jsonapi_obscurity']['description'] = t('Please set the parameter %parameter in the file %file.', [ | ||
'%parameter' => 'jsonapi_obscurity.prefix', | ||
'%file' => 'sites/default/services.yml', | ||
]); | ||
$requirements['jsonapi_obscurity']['severity'] = REQUIREMENT_ERROR; | ||
} | ||
} | ||
|
||
return $requirements ?? []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
parameters: | ||
jsonapi_obscurity.prefix: '' | ||
|
||
services: | ||
jsonapi_obscurity_subscriber: | ||
class: Drupal\jsonapi_obscurity\EventSubscriber\JsonApiObscuritySubscriber | ||
tags: | ||
- { name: event_subscriber } | ||
arguments: | ||
[ '%jsonapi.base_path%', '%jsonapi_obscurity.prefix%' ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
|
||
namespace Drupal\jsonapi_obscurity\EventSubscriber; | ||
|
||
use Drupal\Core\Language\LanguageManager; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Event\RequestEvent; | ||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
|
||
/** | ||
* Event subscriber that handles the JSON:API obscurity prefix. | ||
*/ | ||
class JsonApiObscuritySubscriber implements EventSubscriberInterface { | ||
|
||
/** | ||
* Creates a new JsonApiObscuritySubscriber object. | ||
* | ||
* @param string $jsonApiBasePath | ||
* The JSON:API base path. | ||
* @param string $obscurityPrefix | ||
* The JSON:API obscurity prefix. | ||
*/ | ||
public function __construct( | ||
protected string $jsonApiBasePath, | ||
protected string $obscurityPrefix | ||
) {} | ||
|
||
/** | ||
* Handles incoming JSON:API requests with an obscurity prefix. | ||
*/ | ||
public function handle(RequestEvent $event): void { | ||
$request = $event->getRequest(); | ||
if ($this->applies($request)) { | ||
$this->validatePrefix($request); | ||
$this->reinitializeRequestWithoutPrefix($request); | ||
} | ||
} | ||
|
||
/** | ||
* Decides whether obscurity prefix handling applies. | ||
* | ||
* Resolve the path and check whether it contains the JSON:API base path. | ||
* Additionally, check if the obscurity prefix is non-empty. | ||
*/ | ||
protected function applies(Request $request): bool { | ||
return !empty($this->obscurityPrefix) && | ||
str_starts_with($this->getPlainPath($request), $this->jsonApiBasePath . '/'); | ||
} | ||
|
||
/** | ||
* Validates obscurity prefix in the requested path. | ||
*/ | ||
protected function validatePrefix(Request $request): void { | ||
$this->obscurityPrefix = '/' . ltrim($this->obscurityPrefix, '/'); | ||
$path_prefix = strstr($request->getPathInfo(), $this->jsonApiBasePath, TRUE); | ||
if ($path_prefix != $this->obscurityPrefix) { | ||
// Check with potential langcode. | ||
$langcode = substr($path_prefix, strrpos($path_prefix, '/') + 1); | ||
if ( | ||
!array_key_exists($langcode, LanguageManager::getStandardLanguageList()) || | ||
$path_prefix != $this->obscurityPrefix . '/' . $langcode | ||
) { | ||
throw new NotFoundHttpException(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Cuts the obscurity prefix from the path in the request. | ||
*/ | ||
protected function reinitializeRequestWithoutPrefix(Request $request): void { | ||
$request->server->set('REQUEST_URI', $this->getBarePath($request)); | ||
// The request has to be reinitialized to set the correct path info. | ||
$request->initialize( | ||
$request->query->all(), | ||
$request->request->all(), | ||
$request->attributes->all(), | ||
$request->cookies->all(), | ||
$request->files->all(), | ||
$request->server->all(), | ||
$request->getContent() | ||
); | ||
} | ||
|
||
/** | ||
* Returns the path without the obscurity prefix. | ||
*/ | ||
protected function getBarePath(Request $request): string { | ||
return preg_replace('/^' . preg_quote($this->obscurityPrefix, '/') . '/', '', $request->getPathInfo()) ?? ''; | ||
} | ||
|
||
/** | ||
* Returns the path without the obscurity prefix and langcode. | ||
*/ | ||
protected function getPlainPath(Request $request): string { | ||
$plain_path = $this->getBarePath($request); | ||
$exploded_path = explode('/', ltrim($plain_path, '/'), 2); | ||
if ( | ||
isset($exploded_path[0]) && | ||
isset($exploded_path[1]) && | ||
array_key_exists($exploded_path[0], LanguageManager::getStandardLanguageList()) | ||
) { | ||
$plain_path = '/' . $exploded_path[1]; | ||
} | ||
return $plain_path; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public static function getSubscribedEvents(): array { | ||
// Choose a high priority because the route will be modified. | ||
$events[KernelEvents::REQUEST][] = ['handle', 980]; | ||
return $events; | ||
} | ||
|
||
} |