Security fixes: CSV injection, policy body output, invite token hashing, slot datetime validation #43
Reference in New Issue
Block a user
Delete Branch "feature/security-review-fixes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes the four actionable findings from the security review pass: #39, #40, #41, #42.
What changed
CSV formula injection in the payments export (#39 — Medium)
PaymentReport::csvLine()now apostrophe-prefixes any field with a leading=,+,-,@, tab, or CR before quoting. Student display names are self-chosen at registration and flow into the CSV the studio admin opens in Excel/Google Sheets — previously a name like=HYPERLINK(...)became a live formula. New test covers hostile names across all trigger characters.Policy bodies sanitised at output (#40 — defense-in-depth)
PolicyEndpoint::index()runs the body throughwp_kses_post()before returning it. The booking/group-class JS renders this HTML raw viainnerHTML; protection previously rested entirely on every write path remembering to kses. Now a missed write path can't become stored XSS against students.Invite tokens hashed at rest (#41)
Only
hash('sha256', $token)is stored inus_invites; the registration flow hashes the submitted token for lookup (Invite::hashToken()). The admin Invites page shows the full registration link once, in a notice right after creation — the pending list now shows email + invited date, and a lost link is re-issued by revoke + re-invite. A database leak (backup, SQLi in an unrelated plugin) can no longer be used to redeem pending invites and mint accounts.Note: existing plaintext pending invites stop matching after this change and must be revoked and re-issued (pre-1.0, no migration).
Availability datetime validation (#42)
New
AvailabilitySlot::normalizeDateTime()accepts canonicalY-m-d H:i[:s]and HTMLdatetime-local(Y-m-d\TH:i[:s]) forms, canonicalises toY-m-d H:i:s, and rejects everything else via a strict round-trip check (no PHP date rollover:2026-02-30is refused). Both creation paths use it — REST returns400 invalid_datetime, the admin form no-ops like the existing empty-field check — and both now also requireend > start. Previously garbage reached the DATETIME column on the single path and threw an unhandled exception inside the weekly-series date arithmetic.Checks
composer test(204 tests, 594 assertions), PHPStan L6, and PHPCS all green. Feature docs updated (account-registration, payment-reporting, availability-management).🤖 Generated with Claude Code