You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[security] Rotating an encrypted setting silently does nothing — the new ciphertext is written but sys_setting.value_enc still points at the old handle, so a leaked credential stays live after a 200 #8030
Found by a retest of platform-core.settings-hub-roundtrip (QA run #7514) against origin/main @ b602d536, rebuilt — specifically while checking the #7522/PR #7554 secret-redaction fix for over-correction. That fix is verified correct and is not the cause; see the attribution section.
Impact
A secret rotation appears to succeed and does not happen.PUT /api/settings/<ns> with a new value for an encrypted key returns 200 with a correctly redacted body, sys_setting.updated_at advances, and a new sys_secret row containing the new plaintext is inserted — but sys_setting.value_encstill points at the first handle, so the effective stored secret is still the original value.
The operationally dangerous shape: rotating a leaked SMTP password or provider API key looks like it worked — 200, masked read-back, audit row — while the old credential remains the one in force. Every signal an admin can see says the rotation landed.
The first write of any secret is correct, so this is invisible until the second one.
Reproduction (4×)
Sign in as admin.
PUT /api/settings/sms {provider:'twilio', twilio_account_sid:'AC1', twilio_from:'+15550000000', twilio_auth_token:'alpha'} → 200.
Read sys_setting (namespace=sms, key=twilio_auth_token): value_enc = sec_A. Decrypt sys_secret[sec_A] (AES-256-GCM, dev data key, AAD sms|twilio_auth_token) → alpha. ✅ correct.
PUT /api/settings/sms {twilio_auth_token:'beta'} → 200, correctly redacted body.
Re-read: sys_setting.updated_athas advanced; a newsys_secret row exists whose ciphertext decrypts to beta; but sys_setting.value_enc is still sec_A, so the effective secret is still alpha.
Expected: value_enc repointed to the new handle; effective secret beta. Actual: rotation lost; orphan ciphertext rows accumulate (observed 7 → 8 → 9 across three writes).
Reproduced on mail.smtp_password (×2), storage.s3_secret_access_key, and sms.twilio_auth_token (×2, including after a server restart).
The engine strips readonly fields from a non-system caller's UPDATE payload (stripReadonlyFields, packages/objectql/src/engine.ts:8504 and :8651), so SettingsService.upsertRow's engine.update(objectName, { where, data: { ...row } }) (packages/services/service-settings/src/settings-service.ts:1857) can never repoint the handle.
The INSERT path is unaffected — which is exactly why the first write of a secret is correct and every later one is not.
The readonly: true flag dates to 2026-08-07 (b3c1f3cd); the redaction landed 2026-08-11 (23bc6e16).
The redaction only drops keys whose submitted value equals the mask. In this reproduction the PUT carries a genuinely new value, and the response echoes the key rather than dropping it — so the mask-drop path is visibly not involved.
Acceptance criteria
A second (and third) PUT of a new value for an encrypted key repoints sys_setting.value_enc so the effective secret is the new one; a read-after-write through the service resolves the new plaintext.
The mask-echo no-op is preserved: PUT with •••••••• still leaves the stored ciphertext byte-identical (this currently works and must not regress).
No orphan sys_secret rows are left behind by a rotation — or, if orphans are acceptable, they are reaped.
The first-write path is unchanged.
What is verified working (scoping this card)
The #7522 redaction fix itself is sound and should not be touched:
Both flavours read back as •••••••• — type:'password' (mail.smtp_password) and encrypted:true (mail.api_key) — with every cascadeChain entry masked rather than stripped.
Whole-body literal scan across all 10 namespace bodies andGET /api/settings → zero plaintext hits.
Retest of platform-core.settings-hub-roundtrip clause 10 at b602d536. Storage-layer facts were read by decrypting sys_secret out-of-band with the dev data key, not inferred from the API.
Found by a retest of
platform-core.settings-hub-roundtrip(QA run #7514) againstorigin/main@b602d536, rebuilt — specifically while checking the #7522/PR #7554 secret-redaction fix for over-correction. That fix is verified correct and is not the cause; see the attribution section.Impact
A secret rotation appears to succeed and does not happen.
PUT /api/settings/<ns>with a new value for an encrypted key returns 200 with a correctly redacted body,sys_setting.updated_atadvances, and a newsys_secretrow containing the new plaintext is inserted — butsys_setting.value_encstill points at the first handle, so the effective stored secret is still the original value.The operationally dangerous shape: rotating a leaked SMTP password or provider API key looks like it worked — 200, masked read-back, audit row — while the old credential remains the one in force. Every signal an admin can see says the rotation landed.
The first write of any secret is correct, so this is invisible until the second one.
Reproduction (4×)
PUT /api/settings/sms {provider:'twilio', twilio_account_sid:'AC1', twilio_from:'+15550000000', twilio_auth_token:'alpha'}→ 200.sys_setting(namespace=sms, key=twilio_auth_token):value_enc = sec_A. Decryptsys_secret[sec_A](AES-256-GCM, dev data key, AADsms|twilio_auth_token) →alpha. ✅ correct.PUT /api/settings/sms {twilio_auth_token:'beta'}→ 200, correctly redacted body.sys_setting.updated_athas advanced; a newsys_secretrow exists whose ciphertext decrypts tobeta; butsys_setting.value_encis stillsec_A, so the effective secret is stillalpha.Expected:
value_encrepointed to the new handle; effective secretbeta.Actual: rotation lost; orphan ciphertext rows accumulate (observed 7 → 8 → 9 across three writes).
Reproduced on
mail.smtp_password(×2),storage.s3_secret_access_key, andsms.twilio_auth_token(×2, including after a server restart).Root cause (located)
packages/platform-objects/src/system/sys-setting.object.ts:174declares:The engine strips readonly fields from a non-system caller's UPDATE payload (
stripReadonlyFields,packages/objectql/src/engine.ts:8504and:8651), soSettingsService.upsertRow'sengine.update(objectName, { where, data: { ...row } })(packages/services/service-settings/src/settings-service.ts:1857)can never repoint the handle.
The INSERT path is unaffected — which is exactly why the first write of a secret is correct and every later one is not.
Not a regression of PR #7554
readonly: trueflag dates to 2026-08-07 (b3c1f3cd); the redaction landed 2026-08-11 (23bc6e16).Acceptance criteria
PUTof a new value for an encrypted key repointssys_setting.value_encso the effective secret is the new one; a read-after-write through the service resolves the new plaintext.PUTwith••••••••still leaves the stored ciphertext byte-identical (this currently works and must not regress).sys_secretrows are left behind by a rotation — or, if orphans are acceptable, they are reaped.What is verified working (scoping this card)
The #7522 redaction fix itself is sound and should not be touched:
••••••••—type:'password'(mail.smtp_password) andencrypted:true(mail.api_key) — with everycascadeChainentry masked rather than stripped.GET /api/settings→ zero plaintext hits.value: null(not omitted — QA checklist:platform-core.settings-hub-roundtrip's secret clause says what must NOT be returned but never what IS — it cannot distinguish masked from omitted #7573 shape holds).••••••••is stored and served verbatim.source:'env',locked:true,lockedReason, and still refusePUTwith 409 SETTINGS_LOCKED.settings-routes.test.ts+settings-secret-redaction.test.ts→ 29/29.Source
Retest of
platform-core.settings-hub-roundtripclause 10 atb602d536. Storage-layer facts were read by decryptingsys_secretout-of-band with the dev data key, not inferred from the API.