Skip to content

Commit 82a197e

Browse files
committed
Backport Ability_Function_Resolver API enhancement to harden security.
1 parent 97f9662 commit 82a197e

2 files changed

Lines changed: 274 additions & 61 deletions

File tree

src/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
/**
1717
* Resolves and executes WordPress Abilities API function calls from AI models.
1818
*
19+
* This class must be instantiated with the specific abilities that the AI model
20+
* is allowed to execute, ensuring that only explicitly specified abilities can
21+
* be called. This prevents the model from executing arbitrary abilities.
22+
*
1923
* @since 7.0.0
2024
*/
2125
class WP_AI_Client_Ability_Function_Resolver {
@@ -28,6 +32,35 @@ class WP_AI_Client_Ability_Function_Resolver {
2832
*/
2933
private const ABILITY_PREFIX = 'wpab__';
3034

35+
/**
36+
* Map of allowed ability names for this instance.
37+
*
38+
* Keys are ability name strings, values are `true` for O(1) lookup.
39+
*
40+
* @since 7.0.0
41+
* @var array<string, true>
42+
*/
43+
private array $allowed_abilities;
44+
45+
/**
46+
* Constructor.
47+
*
48+
* @since 7.0.0
49+
*
50+
* @param WP_Ability|string ...$abilities The abilities that this resolver is allowed to execute.
51+
*/
52+
public function __construct( ...$abilities ) {
53+
$this->allowed_abilities = array();
54+
55+
foreach ( $abilities as $ability ) {
56+
if ( $ability instanceof WP_Ability ) {
57+
$this->allowed_abilities[ $ability->get_name() ] = true;
58+
} elseif ( is_string( $ability ) ) {
59+
$this->allowed_abilities[ $ability ] = true;
60+
}
61+
}
62+
}
63+
3164
/**
3265
* Checks if a function call is an ability call.
3366
*
@@ -36,7 +69,7 @@ class WP_AI_Client_Ability_Function_Resolver {
3669
* @param FunctionCall $call The function call to check.
3770
* @return bool True if the function call is an ability call, false otherwise.
3871
*/
39-
public static function is_ability_call( FunctionCall $call ): bool {
72+
public function is_ability_call( FunctionCall $call ): bool {
4073
$name = $call->getName();
4174
if ( null === $name ) {
4275
return false;
@@ -48,16 +81,20 @@ public static function is_ability_call( FunctionCall $call ): bool {
4881
/**
4982
* Executes a WordPress ability from a function call.
5083
*
84+
* Only abilities that were specified in the constructor are allowed to be
85+
* executed. If the ability is not in the allowed list, an error response
86+
* with code `ability_not_allowed` is returned.
87+
*
5188
* @since 7.0.0
5289
*
5390
* @param FunctionCall $call The function call to execute.
5491
* @return FunctionResponse The response from executing the ability.
5592
*/
56-
public static function execute_ability( FunctionCall $call ): FunctionResponse {
93+
public function execute_ability( FunctionCall $call ): FunctionResponse {
5794
$function_name = $call->getName() ?? 'unknown';
5895
$function_id = $call->getId() ?? 'unknown';
5996

60-
if ( ! self::is_ability_call( $call ) ) {
97+
if ( ! $this->is_ability_call( $call ) ) {
6198
return new FunctionResponse(
6299
$function_id,
63100
$function_name,
@@ -69,7 +106,20 @@ public static function execute_ability( FunctionCall $call ): FunctionResponse {
69106
}
70107

71108
$ability_name = self::function_name_to_ability_name( $function_name );
72-
$ability = wp_get_ability( $ability_name );
109+
110+
if ( ! isset( $this->allowed_abilities[ $ability_name ] ) ) {
111+
return new FunctionResponse(
112+
$function_id,
113+
$function_name,
114+
array(
115+
/* translators: %s: ability name */
116+
'error' => sprintf( __( 'Ability "%s" was not specified in the allowed abilities list.' ), $ability_name ),
117+
'code' => 'ability_not_allowed',
118+
)
119+
);
120+
}
121+
122+
$ability = wp_get_ability( $ability_name );
73123

74124
if ( ! $ability instanceof WP_Ability ) {
75125
return new FunctionResponse(
@@ -113,15 +163,17 @@ public static function execute_ability( FunctionCall $call ): FunctionResponse {
113163
* @param Message $message The message to check.
114164
* @return bool True if the message contains ability calls, false otherwise.
115165
*/
116-
public static function has_ability_calls( Message $message ): bool {
117-
return null !== array_find(
118-
$message->getParts(),
119-
static function ( MessagePart $part ): bool {
120-
return $part->getType()->isFunctionCall()
121-
&& $part->getFunctionCall() instanceof FunctionCall
122-
&& self::is_ability_call( $part->getFunctionCall() );
166+
public function has_ability_calls( Message $message ): bool {
167+
foreach ( $message->getParts() as $part ) {
168+
if ( $part->getType()->isFunctionCall() ) {
169+
$function_call = $part->getFunctionCall();
170+
if ( $function_call instanceof FunctionCall && $this->is_ability_call( $function_call ) ) {
171+
return true;
172+
}
123173
}
124-
);
174+
}
175+
176+
return false;
125177
}
126178

127179
/**
@@ -132,14 +184,14 @@ static function ( MessagePart $part ): bool {
132184
* @param Message $message The message containing function calls.
133185
* @return Message A new message with function responses.
134186
*/
135-
public static function execute_abilities( Message $message ): Message {
187+
public function execute_abilities( Message $message ): Message {
136188
$response_parts = array();
137189

138190
foreach ( $message->getParts() as $part ) {
139191
if ( $part->getType()->isFunctionCall() ) {
140192
$function_call = $part->getFunctionCall();
141193
if ( $function_call instanceof FunctionCall ) {
142-
$function_response = self::execute_ability( $function_call );
194+
$function_response = $this->execute_ability( $function_call );
143195
$response_parts[] = new MessagePart( $function_response );
144196
}
145197
}

0 commit comments

Comments
 (0)