feat(payment): redact provider secrets in admin config API
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.
Treat those fields as write-only from the admin surface:
- decryptAndMaskConfig() strips sensitive keys from the GET response.
The authoritative list is an explicit per-provider registry that
mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
alipay → privateKey, publicKey, alipayPublicKey
wxpay → privateKey, apiV3Key, publicKey
stripe → secretKey, webhookSecret (publishableKey stays plain)
easypay → pkey
Payment runtime still reads the full config via decryptConfig, so
nothing at the gateway changes.
- mergeConfig() treats an empty value for a sensitive key as "leave
unchanged" — the admin UI omits unchanged secrets so operators can
tweak non-sensitive settings without re-entering credentials.
- Admin dialog (PaymentProviderDialog.vue):
* secret inputs get autocomplete="new-password", data-1p-ignore,
data-lpignore and data-bwignore so password managers do not
offer to save provider credentials
* in edit mode the required-field check skips sensitive fields
(empty is the "keep existing" signal) and the placeholder shows
"leave empty to keep" instead of the default example value
* create mode still requires every non-optional field, including
secrets, since there is nothing to preserve
- Unit test renamed to TestIsSensitiveProviderConfigField, covers
the per-provider registry and specifically asserts that Stripe's
publishableKey is NOT treated as a secret.
This commit is contained in:
@@ -97,41 +97,52 @@ func TestValidateProviderRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSensitiveConfigField(t *testing.T) {
|
||||
func TestIsSensitiveProviderConfigField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
field string
|
||||
wantSen bool
|
||||
providerKey string
|
||||
field string
|
||||
wantSen bool
|
||||
}{
|
||||
// Sensitive fields (contain key/secret/private/password/pkey patterns)
|
||||
{"secretKey", true},
|
||||
{"apiSecret", true},
|
||||
{"pkey", true},
|
||||
{"privateKey", true},
|
||||
{"apiPassword", true},
|
||||
{"appKey", true},
|
||||
{"SECRET_TOKEN", true},
|
||||
{"PrivateData", true},
|
||||
{"PASSWORD", true},
|
||||
{"mySecretValue", true},
|
||||
// Stripe: publishableKey is public, only secretKey/webhookSecret are secrets
|
||||
{"stripe", "secretKey", true},
|
||||
{"stripe", "webhookSecret", true},
|
||||
{"stripe", "SecretKey", true}, // case-insensitive
|
||||
{"stripe", "publishableKey", false},
|
||||
{"stripe", "appId", false},
|
||||
|
||||
// Non-sensitive fields
|
||||
{"appId", false},
|
||||
{"mchId", false},
|
||||
{"apiBase", false},
|
||||
{"endpoint", false},
|
||||
{"merchantNo", false},
|
||||
{"paymentMode", false},
|
||||
{"notifyUrl", false},
|
||||
// Alipay
|
||||
{"alipay", "privateKey", true},
|
||||
{"alipay", "publicKey", true},
|
||||
{"alipay", "alipayPublicKey", true},
|
||||
{"alipay", "appId", false},
|
||||
{"alipay", "notifyUrl", false},
|
||||
|
||||
// Wxpay
|
||||
{"wxpay", "privateKey", true},
|
||||
{"wxpay", "apiV3Key", true},
|
||||
{"wxpay", "publicKey", true},
|
||||
{"wxpay", "publicKeyId", false},
|
||||
{"wxpay", "certSerial", false},
|
||||
{"wxpay", "mchId", false},
|
||||
|
||||
// EasyPay
|
||||
{"easypay", "pkey", true},
|
||||
{"easypay", "pid", false},
|
||||
{"easypay", "apiBase", false},
|
||||
|
||||
// Unknown provider: never sensitive
|
||||
{"unknown", "secretKey", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.field, func(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(tc.providerKey+"/"+tc.field, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := isSensitiveConfigField(tc.field)
|
||||
assert.Equal(t, tc.wantSen, got, "isSensitiveConfigField(%q)", tc.field)
|
||||
got := isSensitiveProviderConfigField(tc.providerKey, tc.field)
|
||||
assert.Equal(t, tc.wantSen, got, "isSensitiveProviderConfigField(%q, %q)", tc.providerKey, tc.field)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user