bd-security
better-data: Security-sensitive changes
For library maintainers touching anything in better-data's security perimeter — Secret, EncryptionEngine, #[Sensitive], #[Encrypted], RequestSource guards, password handling. Mistakes in this perimeter aren't bugs that show up in tests; they're regressions that ship plaintext to disk or leak credentials in logs.
Misconception this skill corrects
"I'll add a debug-mode that logs the encrypted value's plaintext when developer mode is on — it's only for development."
Don't. The security-feature-with-bypass is the worst outcome — caller assumes redaction is universal, log infrastructure picks up the "debug" path in production by accident, plaintext lands in CloudWatch / Sentry / wp-debug.log forever. The discipline is no-bypass: Secret's __toString returns '***' (src/Secret.php:84-87), jsonSerialize returns '***' (src/Secret.php:89-92), __debugInfo (controls var_dump / print_r) returns ['value' => '***'] (src/Secret.php:99-103), __serialize THROWS SecretSerializationException (src/Secret.php:105-109).
The throwing __serialize is deliberate — a caller serialized a Secret has already made a security-relevant mistake. Relaxing it to redact instead would silently let the bug ship. The exception forces them to either ->reveal() explicitly (audit point) or rethink the flow.
Other AI-prone misconceptions:
- "I'll cache the encryption key in a static property to avoid re-reading the constant on every call." Wrong —
EncryptionEnginedeliberately re-reads on every call (src/Encryption/EncryptionEngine.php:53-54) so key rotation viaBETTER_DATA_ENCRYPTION_KEY_PREVIOUSactually works. A process-long cache defeats rotation. - "
==and===are fine for comparing twoSecrets; the constant-time stuff is paranoia." Wrong — string compare is timing-dependent and leaks length / first-byte equality through repeated probing.Secret::equalsuseshash_equals(src/Secret.php:78-82). Always. - "If decryption fails, return null and the caller falls back to the default." Wrong — silent failure on decrypt is worse than an exception. A caller that gets
nullinstead of the expected secret may treat it as "user never set a key" and proceed.EncryptionEngine::decryptthrowsDecryptionFailedException(src/Encryption/EncryptionEngine.php:109,130).