fix(security): unbekannte Requirement-ID sperrt das Divi-Tool
Deklariert ein Tool über DeclaresRequirements eine ID, die keine Divi-Fähigkeit ist, wird der Aufruf mit divi_incompatible (423) gesperrt, statt ungesperrt zu laufen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
234fd1edac
commit
691231f776
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\AbilityGuard;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use WP_Error;
|
||||
|
||||
@@ -17,7 +18,8 @@ defined('ABSPATH') || exit;
|
||||
* answers divi_incompatible / divi_untested with the affected capability instead of failing inside Divi. A refused
|
||||
* write is logged with result "refused" (as before); a refused read changes nothing and is not. A tool that declares
|
||||
* nothing (RequiresDiviFeatures, or DeclaresRequirements for lit/content-create without divi) is left alone; the
|
||||
* adapters still check each write themselves.
|
||||
* adapters still check each write themselves. A declared ID that is no Divi capability (FeatureSets::unknownIds())
|
||||
* locks the call as divi_incompatible with details.unknown – the lock fails closed.
|
||||
*/
|
||||
final class DiviFeatureGate implements AbilityGuard
|
||||
{
|
||||
@@ -27,6 +29,11 @@ final class DiviFeatureGate implements AbilityGuard
|
||||
|
||||
public function refusal(Ability $ability, array $input): ?WP_Error
|
||||
{
|
||||
$unknown = FeatureSets::unknownIds($ability, $input);
|
||||
if ($unknown !== []) {
|
||||
return $this->refuseUnknown($ability, $unknown);
|
||||
}
|
||||
|
||||
$lock = ReleaseGate::lockOf(FeatureSets::declaredBy($ability, $input));
|
||||
if ($lock === null) {
|
||||
return null;
|
||||
@@ -45,6 +52,34 @@ final class DiviFeatureGate implements AbilityGuard
|
||||
return $lock->toError();
|
||||
}
|
||||
|
||||
/** @param non-empty-list<string> $unknown Declared IDs that are no Divi capability. */
|
||||
private function refuseUnknown(Ability $ability, array $unknown): WP_Error
|
||||
{
|
||||
$status = ReleaseGate::status();
|
||||
$ids = implode(', ', $unknown);
|
||||
if (! self::readsOnly($ability)) {
|
||||
$this->log?->record(
|
||||
$ability->name(),
|
||||
'divi',
|
||||
$status->version,
|
||||
sprintf('Abgelehnt: %s – unbekannte Fähigkeit %s.', $status->versionLabel(), $ids),
|
||||
result: 'refused'
|
||||
);
|
||||
}
|
||||
|
||||
return Errors::make(
|
||||
'divi_incompatible',
|
||||
sprintf(
|
||||
'%s: Das Werkzeug verlangt unbekannte Fähigkeiten (%s). Das ist gesperrt. Es wurde nichts geändert.',
|
||||
$status->versionLabel(),
|
||||
$ids
|
||||
),
|
||||
'Den Nutzer informieren, dass LIT MCP aktualisiert werden muss.',
|
||||
423,
|
||||
['unknown' => $unknown]
|
||||
);
|
||||
}
|
||||
|
||||
/** A refusal is the whole answer: there is no result to annotate (the lock of reading tools is gone, E2). */
|
||||
public function annotate(Ability $ability, mixed $result): mixed
|
||||
{
|
||||
|
||||
@@ -17,8 +17,8 @@ final class FeatureSets
|
||||
{
|
||||
/**
|
||||
* The capabilities $ability declares for $input: its own diviFeatures() (the Divi tools) or the IDs a core tool
|
||||
* names through DeclaresRequirements (lit/content-create with divi). Anything else declares none; an ID that is no
|
||||
* DiviFeature is ignored (the core may name capabilities of other integrations).
|
||||
* names through DeclaresRequirements (lit/content-create with divi). Anything else declares none. Every declared
|
||||
* ID is a Divi ID; one that is no DiviFeature is skipped here but locks the call (unknownIds(), fail closed).
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return list<DiviFeature>
|
||||
@@ -44,6 +44,25 @@ final class FeatureSets
|
||||
return self::merge($features);
|
||||
}
|
||||
|
||||
/**
|
||||
* The IDs a core tool declares for $input that are no DiviFeature. Such a call cannot be judged, so
|
||||
* DiviFeatureGate refuses it instead of letting it run unlocked.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function unknownIds(Ability $ability, array $input): array
|
||||
{
|
||||
if ($ability instanceof RequiresDiviFeatures || ! $ability instanceof DeclaresRequirements) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
$ability->requirements($input),
|
||||
static fn (string $id): bool => DiviFeature::tryFrom($id) === null
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<DiviFeature> ...$lists
|
||||
* @return list<DiviFeature> Every capability once, in the order of first appearance.
|
||||
|
||||
Reference in New Issue
Block a user