Opened 7 weeks ago
Last modified 79 minutes ago
#64789 new task (blessed)
Security audit for API key storage on the Connectors screen
| Reported by: |
|
Owned by: | |
|---|---|---|---|
| Milestone: | 7.0 | Priority: | normal |
| Severity: | normal | Version: | trunk |
| Component: | Security | Keywords: | 2nd-opinion dev-feedback |
| Focuses: | ui, administration, rest-api, privacy | Cc: |
Description
The Connectors screen introduced in #64730 stores AI provider API keys using the WordPress options API. Several security concerns were discussed during the review that need to be verified.
Plaintext storage in the database
API keys for AI providers are currently stored as plaintext option values. These are sensitive credentials that, in the perfect world, should be encrypted. WordPress ships with libsodium (via sodium_compat), which could be used for authenticated encryption.
Masking bypass via /wp-admin/options.php
The current masking approach uses a filter on option_{name} to redact the key value in REST responses and on the Connectors page. However, the key is still visible in plaintext when visiting the hidden options.php screen in WP Admin. The masking filter is also temporarily removed when passing the key to the AI provider class for authentication, which may create additional windows of exposure.
Key portability concerns
As noted in review, just because a user is authorized to connect a provider on a site doesn't mean the key should be extractable. Encryption at rest (where only the site itself can decrypt for authenticated API calls) would mitigate this.
Proposed actions
- Evaluate using Sodium-based encryption for API key values stored in the options table.
- Ensure masking is applied consistently across all surfaces (REST API, options.php, wp_options database queries) or that encryption makes masking less critical.
- Document the threat model: who are we protecting against (other admins, database access, plugins reading options)?
Change History (12)
#2
@
6 weeks ago
+1 to everything that @johnbillion says. While it would be great to have encryption, the implications for Core are different than for a plugin.
The challenge is indeed that we need to rely on a secret, and some sites are configured to rotate these (which would then lead to the data no longer being decryptable).
So at the minimum, we would need a new constant for an encryption key, and that would need to be explicitly documented as to never rotate its value. Certainly not trivial to pull off at the scale of WordPress, and certainly not this late in the release cycle.
It's also worth noting that, if a malicious plugin gets access to the site, none of it helps. So I think it's fair to proceed with the current baseline of no encryption, and explore support for external two-way encryption systems in the future.
The only thing that I think we should fix here is the display in plaintext in wp-admin/options.php. That's of course not encryption, but a small tweak we should add in for parity with how these values are displayed elsewhere.
This ticket was mentioned in Slack in #core-passwords by johnbillion. View the logs.
6 weeks ago
#4
follow-up:
↓ 5
@
6 weeks ago
The only thing that I think we should fix here is the display in plaintext in wp-admin/options.php. That's of course not encryption, but a small tweak we should add in for parity with how these values are displayed elsewhere.
@jorgefilipecosta is working on the fix in https://github.com/WordPress/wordpress-develop/pull/11158. He even created a dedicated ticket #64793 for that, as it looks like we will move this one to the 7.1 release.
#5
in reply to:
↑ 4
@
6 weeks ago
Replying to gziolo:
The only thing that I think we should fix here is the display in plaintext in wp-admin/options.php. That's of course not encryption, but a small tweak we should add in for parity with how these values are displayed elsewhere.
@jorgefilipecosta is working on the fix in https://github.com/WordPress/wordpress-develop/pull/11158. He even created a dedicated ticket #64793 for that, as it looks like we will move this one to the 7.1 release.
An alternative to this would be to store all the keys in a single connectors_api_keys option, which would result in them getting stored as a serialized array, and thus appear as SERIALIZED DATA on the options.php screen.
This ticket was mentioned in Slack in #core-ai by justlevine. View the logs.
6 weeks ago
#7
@
6 weeks ago
This came up the other day in a discussion about the Two Factor plugin and how we'd store a key for encrypting TOTP tokens in the database. It got me thinking, and I started working on a potential option to add a get_secret() function (and related utility) to WP.
I spun this up as a plugin (https://github.com/ericmann/secrets-manager/) and submitted it to the .org repo for review. It's still a first pass and needs some more attention, but provides similar utility to Kubernetes' secrets system, just in WordPress. Secrets are automatically encrypted either with a fixed key set in wp-config.php or using the Site Kit approach of concatenating existing salts from the config. It also has utility for rotation and WP CLI integration to make it more usable.
This ticket was mentioned in Slack in #core-ai by dkotter. View the logs.
5 weeks ago
This ticket was mentioned in Slack in #core by audrasjb. View the logs.
4 weeks ago
#12
@
2 hours ago
- Focuses ui administration rest-api privacy added
- Keywords 2nd-opinion dev-feedback added
I’ve been following this discussion while researching and testing security issues around Connectors, and I wanted to add one observation that seems worth separating from the read-side masking discussion, which has already been covered well. I’ve collected some of my notes and test results here in case they are useful as supporting context: Connectors API reference and security notes.
What stands out to me is the write side. In WordPress 7.0 RC2, connector credentials are ordinary REST-writable settings behind the broad manage_options gate. Database-backed keys can be replaced through POST /wp/v2/settings, and invalid AI-provider keys are cleared server-side while the raw REST write still returns 200 OK. The stock Connectors UI does mitigate some of the UX risk: it surfaces invalid-save errors and marks env/constant-backed connectors as externally configured and read-only, but the underlying model still seems to treat billing-capable, prompt-routing credentials more like generic settings than like other high-consequence secrets.
That feels like a write-side integrity / governance gap rather than a read-side disclosure issue. There is no connector-specific capability boundary, no connector-specific change signal or audit hook, and no built-in notification when a key is replaced. For same-provider swaps, the provider hostname does not even change; what changes is whose account receives the requests, who gets billed, and who can inspect the prompts.
(The specific abuse/attack scenario that sticks with me is not an external threat actor but an internal user who realizes manage_options is sufficient to swap keys to their own AI account, exfiltrate customer or other internal prompt data to it, and then swap back the company keys at will, all with very little signal that anything is amiss.)
Core already distinguishes some credential or access-granting changes from ordinary settings writes. Connector credentials seem closer to that category than to routine site settings, especially given that AI keys can carry both billing consequences and prompt-visibility consequences.
If this is considered in scope for the current ticket, a few small additive mitigations seem like they could help a lot:
- Store/display a fingerprint alongside each key.
- Fire a connector-specific change hook carrying old/new fingerprint, actor, and timestamp.
- Optionally notify administrators when a key is changed.
- Introduce a narrower capability such as
manage_connectors. - Preserve the existing UI indication when a key comes from an environment variable or PHP constant, and make database-backed keys equally explicit.
At a minimum, the risks should be clearly documented and emphasized in the UI. For example:
Important: Connector API keys are more sensitive than most settings. Changing a key can affect which account receives AI requests, who is billed for them, and who can view the prompts sent through that connector. Keys set through environment variables or PHP constants, such as in
wp-config.php, override keys saved in the database through this settings screen.
If that is out of scope here, I’d be happy to open a separate ticket focused specifically on credential rotation visibility, auditability, and capability scoping.
There's currently no way to store a two-way encrypted value in WordPress without tying it either to one of the built-in secret keys in wp-config.php or tying it to a new secret. Tying it to a key in this file risks data loss if it gets rotated or a user migrates/exports/copies their data to another environment that uses different keys.
Felix has written about how the Google Site Kit plugin encrypts API keys, but it still relies on a secret key in wp-config.php.
There's nothing that can be done about this in time for 7.0. In the past I've floated the idea of a two-way encryption API in WordPress which uses no encryption by default but allows site owners and hosts to connect it to an environment variable for local two-way encryption, a KMS, a secret store, or any other opt-in means of encryption that reduces the opportunity for data loss.
CC @kasparsd @snicco @rmccue @flixos90