Repository navigation
v6.29.0
Security
-
⚠ Client IP addresses were written to
debug.login full (#1441):Debug::redact_sensitive_data()masked 17 sensitive keys andipwas the one PII key missing, so seven call sites logged a raw address. The fix is at the sink rather than at the seven: the IP keys now take a salted hash and are relabelledip_hash. Masking keeps two characters at each end, and an unsalted hash of an IPv4 is reversible by exhaustion. The address only ever reacheddebug.log, never a page or an email, and only while the area toggle was on; the capability-gatedffc_activity_log.user_ipcolumn is unchanged. -
⚠ A magic token in a URL fragment reached
debug.login full (#1453):url_carries_secret()matchestoken=anywhere in a string, so the URL was recognised as carrying a secret and handed to a stripper whose pattern required?or&before the parameter — and a magic link carries its token in the fragment by design (/valid/#token=...), so the detector accepted it and nothing redacted it. A 64-character bearer token that opens a certificate with no authentication, in a file underwp-content.#is now a leading delimiter as well as a terminator; the value still stops at#, so a query secret does not swallow the fragment after it. Found on the testes host with every area enabled, immediately after #1441 — the existing test used the query shape, which worked throughout, and the shape the plugin actually emits had none.
Fixed
-
The public CSV gate in
ownermode could never match, and a CPF that could not be hashed opened it (#1443): it stripped the author'sffc_user_cpfto digits, but that meta is always av2:envelope — so it compared digits scraped out of base64 against an 11-digit CPF, and every visitor was told their CPF did not match the author's. It now compares the author's storedcpf_hashagainst the typed CPF's hash, both throughSensitiveFieldRegistry. An author with no stored hash fails the gate rather than opening it:hash_equals( '', '' )is TRUE. -
Two activity-log columns were written by the code and declared by nobody (#1444):
flush_buffer()wrotecontext_encryptedandsubmission_idand noCREATE TABLEorALTERdeclared either, so since 6.6.4 both writes were silently dropped on every install. An entry with sensitive context was stored with neither plaintext nor ciphertext; theactivity_log_clear_plaintextcard read 100% complete over a condition it could not evaluate; and the public verification page omitted the recorded-schedule block of any certificate whose window an operator had overridden. Both are now declared andDB_VERSIONmoved, which is what reaches an install that will never be activated again. No row was damaged. -
Two activity-log context keys named identifiers they do not carry (#1448):
IdentityAdoptionlogged 12-character hash prefixes undercpfandrf, andcontains_sensitive()matches on the key name alone — so every adoption encrypted two prefixes and a boolean, and the log screen, which prints raw JSON with no label map, named a document the row never held. Renamed tocpf_prefix/rf_prefix, with a guard over the rule rather than the rename: a sensitive-named context key is registered with its reason or it is a defect. -
An identity merge left the person's audience access on the emptied login (#1368): it moved submissions, appointments and candidacies and never touched
audience_members,audience_booking_usersoraudience_schedule_permissions— so somebody merged onto a surviving account kept logging in with their bookable groups and their schedule permissions attached to the login they no longer use. The same shape as #1367, which cost 1,478 accounts certificates they owned and could not open. All three now move inside the merge's transaction, and because each carriesUNIQUE KEY (<parent>, user_id)a pairing the survivor already holds is dropped rather than updated — reported beside the moves, since a total alone cannot be told apart from a row that went missing. -
The two
html/migration cards need the folder they read (#1438): the plugin stopped shippinghtml/in 6.23.0 and the rewrite migration side-loads images from there, so its repair became impossible while its pending count — which reads the database — did not. Pressing the button walked every affected post, repaired none and dropped pending to zero, leaving the card reading 100% complete over broken content. Both cards are now gated on the folder existing, which hides them and closes the AJAX endpoint together. -
A logged filename carried the auth code the same payload masked (#1453):
PdfGeneratorloggedauth_code, masked by the sink, besidefilename— which iscertificado_{form_id}_{auth_code}.pdf— one line below it. The sink cannot fix this and must not: a CSV export's filename carries no code, so the redaction belongs at the call site that holds it. The guard over the rule freezes the register of payloads carrying afilenamekey, so a scan finding nothing cannot read as clean. Writing it surfaced a latent defect in the scanner both this guard and #1441's read through: tracking quote parity means an apostrophe in a comment opens a string, so the paren match ran past its own payload into the next array. It tokenises comments away first.
Changed
-
A
decrypt_failurewarning now says which call site produced it (#1441): the context was a length and a v2 flag, so the three observed in production named a failure and no path. It carries the calling class and method — arguments deliberately dropped, since on those frames they are the ciphertext — and the realget_current_user_id()instead of a hard-coded0, which had made every entry read as an anonymous request. -
A column an insert names and no declarer names now fails CI (#1447): the defect #1444 fixed was invisible to every existing guard, because they compare two declarations against each other and both columns were named only by their readers. This compares the other way — per table where the table resolves statically, tree-wide for the ten write sites whose table is a runtime value, each registered with its reason rather than skipped. No product code changed.
-
Ciphertext read and used as plaintext now fails CI (#1446): the shape #1443 shipped — a
v2:envelope handed tonormalize_cpf_rf()— matched none of the three identity guards, which watch who hashes or digit-strips an identifier, not who reads one. Two rules per statement, both at zero. The general dataflow version was built five times, is not shipped, and the verdict is on the issue: each attempt reported a clean tree for a different wrong reason. No product code changed. -
Seven guards' self-checks were bare floors, and a bare floor is not a detector (#1428): measured, a collector silently returning half its population left every one of them green — the #1423 shape. Each now holds an invariant a partial scan cannot satisfy: a named member the sort drops first, an independent recount by a different traversal, or a share of an independently counted population.
CLAUDE.mdranks those shapes and names the condition for each, because a loose floor is what a tight floor becomes as the population grows underneath it. No product code changed. -
22 test classes stopped forking a process to isolate nothing, and nine moved the fork to the methods that need it (#1432): ~0.19s of bootstrap per forked method, which 3,034 of the suite's 8,652 tests were paying; the nine run in 11.9s against 54.5s on the same 291 tests. The full suite, not a scan, said which ten of the 32 candidates keep the fork — and the move is safe for the reason that is easy to get backwards: an alias mock created in a child process never reaches the parent, so annotating only a class's hot methods is what protects its cold ones. No product code changed.
-
Twelve self-checks and four exception lists in the test suite stopped being bare floors (#1435): a list that excuses something is read to skip an entry, so a stale entry makes the guard narrower and says nothing; three had no second direction at all. Each is now an exact comparison against an independent traversal, and that recount found a real defect on its first run — reading raw source, it matched the function name inside two docblocks. No product code changed.
-
Two unreproducible figures and twelve numbered self-references corrected in
CLAUDE.md(#1435): the pcov paragraph claimed "101 of the 388@coversfiles" carry a preload; measured, it is 242 in 193 of 428, and 101 is no reading at all — the file's own opening rule failing inside the file. With all 242 stripped and each class run alone twice, every one attributes the same covered-statement count either way, so new tests stop adding one. Docs only.