The Exploit
Scenario: An unauthenticated attacker makes a single request to the WordPress REST API endpoint that exposes Amelia's settings object.
GET /wp-json/ameliabooking/v1/settings HTTP/1.1
Host: target-site.local
Accept: application/json
Observation: The response body contains an unredacted JSON object holding API keys, OAuth access tokens, and payment gateway credentials in plaintext. The attacker extracts googleAccessToken, outlookAccessToken, gMapApiKey, and payment processor secrets (Stripe, Square, Barion keys) without authentication or privilege check.
What the Patch Did
Before:
'gMapApiKey' => $this->getSetting('general', 'gMapApiKey'),
'googleAccessToken' => $this->getSetting('googleCalendar', 'accessToken'),
'outlookAccessToken' => $this->getSetting('outlookCalendar', 'accessToken'),
'whatsAppPhoneID' => $this->getSetting('notifications', 'whatsAppPhoneID'),
'whatsAppAccessToken' => $this->getSetting('notifications', 'whatsAppAccessToken'),
'whatsAppBusinessID' => $this->getSetting('notifications', 'whatsAppBusinessID'),
'livePOSKey' => $this->getSetting('payments', 'barion')['livePOSKey'],
'sandboxPOSKey' => $this->getSetting('payments', 'barion')['sandboxPOSKey'],
After:
'gMapApiKey' => !empty($this->getSetting('general', 'gMapApiKey')),
'googleAccessToken' => !empty($this->getSetting('googleCalendar', 'accessToken')),
// 'outlookAccessToken' removed entirely
'whatsAppEnabled' => Licence\Licence::isFeatureEnabledWithLicense(
'whatsapp',
$this->getSetting('featuresIntegrations', 'whatsapp')
) &&
!empty($this->getSetting('notifications', 'whatsAppPhoneID')) &&
!empty($this->getSetting('notifications', 'whatsAppAccessToken')) &&
!empty($this->getSetting('notifications', 'whatsAppBusinessID')),
// Payment keys removed; replaced with feature flag checks
The patch replaced all secret-bearing fields with boolean presence checks using the !empty() operator. Instead of serializing the actual API key or token value, the endpoint now returns only a true/false indicator of whether the credential is configured. Sensitive fields like whatsAppAccessToken, livePOSKey, and sandboxPOSKey were removed from the public response entirely and their presence is inferred indirectly through feature-enabled flags.
Root Cause
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
The SettingsStorage class's public-facing REST endpoint returns a settings object constructed by calling getSetting() directly on secret keys without filtering. The endpoint does not gate access to the response by user role or nonce; it executes in the REST API's unauthenticated context. An attacker reaches this sink by issuing any GET request to /wp-json/ameliabooking/v1/settings, triggering the settings serialization. The dataflow is: attacker's HTTP GET → WordPress REST router → endpoint handler → getSetting('googleCalendar', 'accessToken') and sibling calls → plaintext token in JSON body → attacker reads response.
Why It Works
The load-bearing line is !empty($this->getSetting(...)). Removing this check would leave the bug entirely exploitable because the sensitive value would be serialized into JSON again. The engineer added the boolean cast to break the semantic link between the configuration truth and its secret representation: the client can now know "yes, Google Calendar is connected" without learning the token itself. The redundant feature-flag checks (Licence::isFeatureEnabledWithLicense()) serve as defence-in-depth, but they do not replace the core control—only the !empty() abstraction prevents token leakage. The removal of fields like outlookAccessToken and payment processor keys is a second layer: fields that carry secrets are stripped from the response schema entirely, so no boolean check can salvage them if the code path reaches serialization.
Hardening Checklist
-
Separate secrets from UI state: Move all API keys, tokens, and credentials into a dedicated, access-controlled storage layer. Query this layer only from internal (non-REST) functions. Public endpoints should reference only feature flags or presence booleans, never the secret values themselves.
-
Apply
wp_verify_nonce()and capability checks to all REST endpoints that touch settings. Even endpoints designed for the frontend should includecurrent_user_can('manage_options')or a nonce validation for POST/PUT operations. For GET, consider requiring an authenticated user withreadcapability at minimum. -
Use WordPress's Settings API (
register_setting()withsanitize_callbackandshow_in_rest => false) for all secrets. This ensures credentials are never auto-exposed via REST unless explicitly marked and then only after capability checks. -
Audit all
getSetting()call sites in your codebase. For each call that touches a key matching*Token*,*Key*,*Secret*,*Password*,*Credential*, wrap the return value in a check before serialization:!empty($value)orisset($value). -
Implement response filtering middleware in your REST controller. Before returning a settings object, strip any key whose name hints at sensitivity. Use an allowlist of safe keys rather than a blacklist.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-40789