Stop the settings sanitizer corrupting the stored client secret - #6
Merged
Conversation
register_setting() attaches Settings::sanitize() to the
sanitize_option_{$option} filter, and WordPress runs that filter on every
update_option() for the option — not just on the settings form submit.
That includes this plugin's own internal writes in Settings::set() and
Settings::migrate_legacy().
Because the sanitizer encrypted the client secret unconditionally, each of
those writes re-encrypted the already-encrypted value, one layer at a time.
After a couple of saves the secret no longer decrypted, and the site lost
its Procore connection with no indication why. Measured in a live install:
after the admin screen registered the setting, a single migrate_legacy()
write produced a triple-encrypted value.
Encryption is now idempotent at that boundary. Encryption::is_encrypted()
recognises cipher text by its prefix, and sanitize() passes such a value
through untouched while still encrypting genuine plaintext from the form.
Two regression tests cover it, and both were confirmed to fail against the
previous behaviour before the fix was applied:
- sanitize() fed its own output repeatedly still yields the same plaintext
- a settings write followed by three internal writes leaves the secret usable
This was only reachable with the setting registered, which is every real
admin request but was not the case in the earlier integration run — that
run never called register_setting(), which is why it passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
register_setting()attachesSettings::sanitize()to thesanitize_option_{$option}filter. WordPress runs that filter on everyupdate_option()for the option — not just on the settings-form submit. That includes the plugin's own internal writes inSettings::set()andSettings::migrate_legacy().The sanitizer encrypted the client secret unconditionally, so each of those writes re-encrypted the already-encrypted value, one layer at a time. After a couple of saves the secret no longer decrypted and the site silently lost its Procore connection, with no indication why.
Measured in a live install — after the admin screen registered the setting, a single
migrate_legacy()write produced a triple-encrypted value:Worst case this loses a 1.x upgrader's migrated credential during the upgrade itself.
The fix
Encryption is idempotent at that boundary.
Encryption::is_encrypted()recognises cipher text by its prefix;sanitize()passes such a value through untouched while still encrypting genuine plaintext from the form.Tests
Two regression tests, both confirmed to fail against the previous behaviour before the fix was applied (I reverted the fix, watched them fail, then restored it):
sanitize()fed its own output repeatedly still yields the same plaintext82 tests, 340 assertions, all green.
Why the earlier integration run missed it
That run never called
register_setting()— under WP-CLIis_admin()is false, soSettingsPage::register()never fires and the sanitize filter was never attached. The faulty path is unreachable without it, and reachable in every real admin request. This run registered the setting explicitly, which is what exposed it.Full test pass on this branch
WordPress-Extra+WordPress-Docs🤖 Generated with Claude Code