Make WordPress Core

Opened 3 weeks ago

Closed 2 weeks ago

Last modified 2 weeks ago

#64819 closed defect (bug) (fixed)

Revisit AI provider API key masking & validation

Reported by: swissspidy's profile swissspidy Owned by: jorgefilipecosta's profile jorgefilipecosta
Milestone: 7.0 Priority: normal
Severity: normal Version: trunk
Component: AI Keywords: has-patch
Focuses: Cc:

Description

_wp_connectors_is_ai_api_key_valid in the sanitize_callback is a bit annoying because it means I can't just do wp option update connectors_ai_openai_api_key 1234 in WP-CLI, because it won't pass validation

It's also not typical for a command like wp option get connectors_ai_google_api_key to not return the actual raw value.

I also can't do wp option update connectors_ai_google_api_key "" to set the option to an empty value, because, again, the validation fails. I have to delete the option, which is not great for caching / db queries.

Soo... can we do this masking and validation somewhere else? And not in a weird way like that _wp_connectors_validate_keys_in_rest hack btw

Change History (9)

#1 @swissspidy
3 weeks ago

Posting what I shared on Slack already, this is what happens due to all this custom logic in place:

$ wp option update connectors_ai_google_api_key <VALIDKEY>
Success: Updated 'connectors_ai_google_api_key' option. # So far so good

$ wp option update connectors_ai_google_api_key <SAMEKEY>
Error: Could not update option 'connectors_ai_google_api_key'. # Here you would expect "Success: Value passed for 'connectors_ai_google_api_key' option is unchanged."

$ wp option update connectors_ai_google_api_key ""
Success: Value passed for 'connectors_ai_google_api_key' option is unchanged. # Nothing actually changed

$ wp option get connectors_ai_google_api_key
<masked key>

#2 @swissspidy
3 weeks ago

Also as an FYI:

wp option list uses a direct db query to get the full list of options, so you will always get the unmasked API key there.

#3 @swissspidy
3 weeks ago

  • Keywords needs-patch added
  • Version set to trunk

It also doesn't help that none of the AI client HTTP requests go through the WP HTTP API, so I cannot hook into those and filter them to short-circuit validation.

There's no filter in _wp_connectors_is_ai_api_key_valid either.

This ticket was mentioned in Slack in #core-ai by swissspidy. View the logs.


3 weeks ago

#5 @flixos90
3 weeks ago

It also doesn't help that none of the AI client HTTP requests go through the WP HTTP API, so I cannot hook into those and filter them to short-circuit validation.

@swissspidy How do you mean? What do they go through instead? The adapter for WP exists exactly so that they should go through the WP HTTP API.

#6 @gziolo
3 weeks ago

@jorgefilipecosta, do the changes you applied in the Gutenberg plugin cover all the requirements? We still need to sync these changes to WordPress trunk.

This ticket was mentioned in PR #11228 on WordPress/wordpress-develop by @jorgefilipecosta.


2 weeks ago
#7

  • Keywords has-patch added; needs-patch removed

## Summary

Trac ticket: https://core.trac.wordpress.org/ticket/64819

Backports two Gutenberg PRs into WordPress core:

  • Gutenberg #76266 — Adds _wp_connectors_get_api_key_source() to detect whether an API key is configured via environment variable, PHP constant, or database. The UI uses this to show the key source and hide "Remove and replace" for externally configured keys.
  • Gutenberg #76327 — Refactors API key validation and masking from sanitize_callback + option_ filters into a single rest_post_dispatch handler (_wp_connectors_rest_settings_dispatch), ensuring raw keys are never exposed via REST API responses.

### Changes

  • New function: _wp_connectors_get_api_key_source() — checks env var → PHP constant → database for API key origin
  • New function: _wp_connectors_rest_settings_dispatch() — masks keys in all /wp/v2/settings responses and validates on POST/PUT, reverting invalid keys
  • Removed function: _wp_connectors_get_real_api_key() — no longer needed since masking moved out of option filters
  • Removed function: _wp_connectors_validate_keys_in_rest() — replaced by the new dispatch handler
  • Modified: _wp_connectors_get_connector_settings() — added static memoization and plugin is_installed/is_activated status enrichment
  • Modified: _wp_register_default_connector_settings() — simplified sanitize_callback to sanitize_text_field, removed option mask filter
  • Modified: _wp_connectors_pass_default_keys_to_ai_client() — skips keys from env/constant sources, uses get_option() directly
  • Modified: _wp_connectors_get_connector_script_module_data() — exposes keySource, isConnected, logoUrl, and plugin status (isInstalled, isActivated)

## Test plan

  • [ ] Verify connectors admin screen loads and displays connector data correctly
  • [ ] Test saving a valid API key via the settings REST endpoint — key should be masked in the response
  • [ ] Test saving an invalid API key — key should be reverted to empty
  • [ ] Verify env var and constant-sourced keys are detected and shown with correct source
  • [ ] Run phpunit --group connectors to verify existing tests pass

#8 @jorgefilipecosta
2 weeks ago

  • Owner set to jorgefilipecosta
  • Resolution set to fixed
  • Status changed from new to closed

In 61985:

Connectors: Add API key source detection and refactor REST behaviour/masking.

Add _wp_connectors_get_api_key_source() to detect whether an API key is configured via environment variable, PHP constant, or database. The UI uses this to show the key source and hide "Remove and replace" for externally configured keys.
Replace _wp_connectors_validate_keys_in_rest() and _wp_connectors_get_real_api_key() with a single rest_post_dispatch handler, _wp_connectors_rest_settings_dispatch(), that masks keys in all /wp/v2/settings responses and validates on POST/PUT, reverting invalid keys.
Simplify _wp_register_default_connector_settings() by replacing the closure-based sanitize_callback and option_ mask filter with plain sanitize_text_field, since masking is now handled at the REST layer.
Enrich _wp_connectors_get_connector_script_module_data() to expose keySource, isConnected, logoUrl, and plugin isInstalled / isActivated status to the admin screen.
Update _wp_connectors_pass_default_keys_to_ai_client() to skip keys sourced from environment variables or constants and read the database directly via get_option().
Set _wp_connectors_init priority to 15 so the registry is ready before settings are registered at priority 20.

Backports https://github.com/WordPress/gutenberg/pull/76266.
Backports https://github.com/WordPress/gutenberg/pull/76327.

Props jorgefilipecosta, gziolo, swissspidy, flixos90.
Fixes #64819.

#9 @jorgefilipecosta
2 weeks ago

Thank you for raising this @swissspidy. The issues you noticed should be addressed.
Now masking happens at the REST API level instead of get_option level, so WP CLI returns full keys.
Validation happens when updating a key in REST, and on the data structure passed to the client (in case a key gets revoked). I removed the validation during setting retrieval. WP CLI should be able to set any key now.

Note: See TracTickets for help on using tickets.