安全注意事项
JSON:API 模块旨在将通过 Drupal 的 Entity API、Field API 和 Typed Data API 定义的数据模型,按符合 JSON:API 规范 的方式通过 API 暴露出来,以便与由 Drupal 管理的数据(实体)进行交互。
在此过程中,它会遵循 Drupal 针对这些数据的所有安全措施:
- 遵循实体访问(Entity Access)。
- 遵循字段访问(Field Access)。
- 在修改数据时,遵循验证约束(validation constraints)。
- 遵循
internal
标志(参见关于如何在实体类型定义、字段定义或属性定义上设置它的文档)。
换言之:JSON:API 不会绕过任何既有的安全措施,也不会额外添加自己的安全层;它复用的是 Drupal 的基础能力。
实体类型、字段类型与数据类型中的缺陷可能导致安全漏洞
尽管如此,实现实体类型、字段类型和数据类型以及其访问控制处理器与验证约束的代码中,确实可能存在缺陷。这在很大程度上可归因于 Drupal 的历史包袱:Drupal 最初没有验证约束(validation constraints),而是表单验证回调;在 Drupal 核心中向“先 API”思维的转变或可视为已完成,但对贡献模块(contrib)或自定义模块则无法保证。
这些缺陷可能导致安全漏洞;过去也确实发生过。此类漏洞并不仅限于 JSON:API 模块;它们同样会影响例如 RESTful Web Services 模块,以及任何与 Entity API 交互的 PHP 代码。
然而,由于恶意用户比起访问 PHP API,更容易访问像 JSON:API 或 RESTful Web Services 这样的 HTTP API,因此在这种情况下需要格外小心。与其他 HTTP API 模块不同,JSON:API 默认就具有更大的 API 表面:为了尽可能提升开发者体验,所有非 internal
的实体类型都会默认可用(当然仍会遵循实体访问),这就需要更多安全考量。
六点安全考量
1. 使用稳定贡献模块的重要性
由实体类型、字段类型与数据类型引发的安全漏洞,仅会对发布在 Drupal.org 上、受 安全公告政策覆盖的稳定模块尽快修复。自定义模块与非稳定的贡献模块不在覆盖范围内。 如果你在使用这些模块,请务必格外谨慎。
2. 审计实体与字段访问
无论你是否使用 JSON:API 或任何其他类似 API 的模块,都强烈建议在 Drupal 站点上审计实体访问与字段访问。如果启用了 JSON:API 的写入能力,这一点尤为重要。
3. 仅暴露你需要的内容
当特定资源类型(实体类型 + 包(bundle))不需要被暴露时,在确保对其访问被拒绝之后,你还可以更进一步将其禁用。要禁用某个资源类型或字段,可以在自定义模块中实现一个PHP API,或者使用提供禁用资源类型与字段 UI 的 JSON:API Extras 贡献模块。这并非总是可行,但在站点所有者同时也拥有所有 API 客户端的情况下,你可以这样做以尽可能缩小 API 表面。
4. 只读模式
如果你的场景只需要读取数据,你可以在 /admin/config/services/jsonapi
启用 JSON:API 的只读模式。这能够减轻来自既有验证约束与写入逻辑中假设性、尚未被发现的缺陷所带来的风险。由于大多数现代解耦式 Drupal 架构只需要读取数据,只读模式在默认情况下是开启的。(适用于 Drupal 核心的 JSON:API,以及贡献模块 2.4 及更高版本。)
5. 以隐蔽性实现安全:设置秘密基础路径
JSON:API 的基础路径默认为 /jsonapi
。你可以将其修改为类似 /hidden/b69dhj027ooae/jsonapi
的路径,以降低自动化攻击的有效性。若尚不存在,请创建 sites/example.com/services.yml
并加入如下内容:
parameters:
jsonapi.base_path: /hidden/b69dhj027ooae/jsonapi
6. 通过移除部分路由,限制可创建或可编辑的实体 bundle
如果你只需要通过 JSON:API 创建或更新部分实体 bundle,你可以在自定义模块中实现一个事件订阅器(event subscriber),只保留白名单中的 POST 与 PATCH 路由。此方式在关闭只读模式后生效,并可能需要重建路由(router rebuild)。
在模块的 services.yml 中添加一个服务:
services:
mymodule.route_subscriber:
class: Drupal\mymodule\Routing\JsonapiLimitingRouteSubscriber
tags:
- { name: event_subscriber }
创建事件订阅器。下面的示例还会使得无法通过 JSON:API 删除任何内容:
<?php
namespace Drupal\mymodule\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Class JsonapiLimitingRouteSubscriber.
*
* Remove all DELETE routes from jsonapi resources to protect content.
*
* Remove POST and PATCH routes from jsonapi resources except for those
* we want end users to create and update via the decoupled API.
*/
class JsonapiLimitingRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
$mutable_types = $this->mutableResourceTypes();
foreach ($collection as $name => $route) {
$defaults = $route->getDefaults();
if (!empty($defaults['_is_jsonapi']) && !empty($defaults['resource_type'])) {
$methods = $route->getMethods();
if (in_array('DELETE', $methods)) {
// We never want to delete data, only unpublish.
$collection->remove($name);
}
else {
$resource_type = $defaults['resource_type'];
if (empty($mutable_types[$resource_type])) {
if (in_array('POST', $methods) || in_array('PATCH', $methods)) {
$collection->remove($name);
}
}
}
}
}
}
/**
* Get mutable resource types, exposed to user changes via API.
*
* @return array
* List of mutable jsonapi resource types as keys.
*/
public function mutableResourceTypes(): array {
return [
'node--article' => TRUE,
'node--document' => TRUE,
'custom_entity--custom_entity' => TRUE,
];
}
}
通过额外权限限制对所有 JSON:API 路由的访问
当将 JSON:API 用于后端集成、受限的 API 客户端或其他非公开用例时,可能希望将所有 JSON:API 路由限制为仅特定权限的用户可访问。你可以在上述路由订阅器中添加如下代码片段来实现(或者作为补充措施):
// Limit access to all jsonapi routes with an extra permission.
foreach ($collection as $route) {
$defaults = $route->getDefaults();
if (!empty($defaults['_is_jsonapi'])) {
$route->setRequirement('_permission', 'FOO custom access jsonapi');
}
}
随后在 FOO.permissions.yml 中定义该权限,并将其授予所需的用户角色。
文章来自 Drupal 文档。