Security fix
GET /ars-nova/v1/option returned get_option() verbatim with no masking and no denylist. Asking it for woocommerce_stripe_settings returned the live Stripe secret key in cleartext.
This is the 2026-08-18 incident that the ticketing bridge's Mailchimp endpoint cites in its docblock as the reason it refuses to store credentials — but the route that actually leaked was never fixed, and was still returning raw values on 2026-08-20 when a defect review found it again.
What changed
- Values on
/optionand/theme-modsare walked recursively. Any leaf whose key looks like a credential is masked (secret,password,api_key,*_token,client_secret,webhook,private_key,_key$, …). - Any string whose value has the shape of a known credential is masked even under an innocent key name — Stripe
sk_/rk_/whsec_, GoogleAIza, GitHubghp_, Slackxox*, PEM private keys. - Masking is never silent. Responses carry a
maskedarray naming the dot-paths withheld, so a caller can tell "this field is empty" from "this field was hidden from you". - No override parameter, deliberately. An endpoint with
?show_secrets=1is an endpoint that leaks secrets. Humans read raw credentials in wp-admin. /plugin-fileis intentionally not scrubbed — it returns code, and masking it would defeat the "what is actually running?" purpose it was added for in 1.2.0.
Verified on staging
woocommerce_stripe_settings→secret_key,test_secret_key,publishable_key,webhook_secret,webhook_dataall masked.blogname→ returned unchanged,masked: []. No collateral damage to ordinary diagnostics.
Known cosmetic nit
Credential-named fields that are empty are still listed in masked even though nothing was withheld. Harmless, slightly misleading, to be tidied on the next touch.