Skip to content

Commit

Permalink
Add implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbaese committed May 16, 2022
1 parent e62c4f9 commit a35305c
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
7 changes: 7 additions & 0 deletions jsonapi_obscurity.info.yml
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
30 changes: 30 additions & 0 deletions jsonapi_obscurity.install
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 ?? [];
}
10 changes: 10 additions & 0 deletions jsonapi_obscurity.services.yml
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%' ]
119 changes: 119 additions & 0 deletions src/EventSubscriber/JsonApiObscuritySubscriber.php
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;
}

}

0 comments on commit a35305c

Please sign in to comment.