Repository navigation
v2.1.5
Fixes from the review of 2.1.4.
Security
- Sign-in limit can no longer lock the admin out or tie up PHP. In 2.1.3/2.1.4 a blocked address waited in PHP (up to 10 s, holding a PHP worker and the session lock) and, once that queue was full, even the right token got 429 without being checked; one address could hold every worker of a small host and stall the public forms. Now PHP never waits: a blocked address still gets one token check per 2 seconds (right or wrong), every other attempt is answered
429at once with the exactRetry-After(1–2 s instead of 900), so the right token gets in at the next free check. A signed-in browser is not affected. A blocked HTML sign-in shows the sign-in form with the wait instead of JSON. - Breaking:
?token=no longer signs a browser in.viewer.php,editor.phpandsandbox.phpignore a token in the URL (login CSRF through a link, and an<img src="viewer.php?token=x">on another site spending your address's wrong-token budget); they show the sign-in form with a notice. Sign in with the form or sendX-BBF-Token. The sandbox has its own sign-in form.submissions.php?token=(scripts, CSV links) keeps working, except for requests a browser marks as coming from another site (Sec-Fetch-Site: cross-site), which cannot use it. - Breaking (since 2.1.3): a redirect target that is only a field (
"redirect": "{{return_url}}") is not followed. Field values inredirectare percent-encoded since 2.1.3 so a respondent cannot change the target (open redirect); such a template led to a relative 404 in 2.1.3/2.1.4. Now it shows the success message instead. Use a fixed target with the value as a parameter, e.g."/thanks?ref={{ref}}". - Upgrade dry run no longer runs unverified package code. Since 2.1.3 a dry run started the package's upgrader and smoke test (with your
config.phploaded) after checking the package only against its own manifest, which proves integrity, not origin. Now package code runs only with--checksum=<SHA-256 from SHA256SUMS>(or--trust-packagefor a package you built yourself). Without it the dry run checks checksums and PHP syntax only, reportscheck: skippedand says how to verify;--applythen uses the installed upgrader. The printed next command keeps--checksum. Takes effect from the next upgrade after 2.1.5 is installed (the runningmaintenance.phpdecides). - Upgrade on Windows: only
maintenance.php(held open by the running upgrade) may be written in place when renaming over it fails; any other file that cannot be renamed (after 5 retries, e.g. while a web request reads it) fails the upgrade and it rolls back, so the web never loads a half-written library. submit.php's daily self-check logs a warning when requests arrive through a proxy (X-Forwarded-For / CF-Connecting-IP) whiletrusted_proxiesis empty (check.php already showed it).
Fixed
- Upgrade with PHP notices: with
display_errors=Onin the CLI (XAMPP, some Windows hosts) one notice, e.g. fromconfig.php, broke the package upgrader's JSON result: the dry run failed, and a notice during--applyreported a finished upgrade as failed. Notices now go to stderr, the result is read after a marker line, and notices are listed underphp_messages. The package side of this fix already applies to the upgrade from 2.1.4 to 2.1.5. smoketest --livenever sends to a made-up address. When an email field has apatternthatsmoke_emaildoes not match, the smoke test used to generate a matching address (e.g.a@firma.sk, a real domain) and the confirmation email went there. Now that form fails with "smoke_email does not match the pattern of email field …" and nothing is submitted; setsmoke_emailto your own address that matches.- Smoke test data: an extreme
pattern((a{1000}){1000}) orminlengthno longer exhausts memory (generated values are capped at 5000 characters); a required field never gets an empty value (^a*$, a "Choose…" option with value""is skipped). - Audit log: a
smoke_tokenshorter than 16 characters is not a credential (live smoke refuses it, check.php/selfcheck warn), so it no longer masks ordinary words such as "contact" in the audit log. - "Back" after a redirect no longer stores a duplicate. Returning from the thank-you page (back/forward cache) showed the form still filled in with the button enabled, and one click stored the same answers again under a new key. The restored form is now cleared (with the success message), and the next fill gets its own key.
- Malformed
show_ifno longer breaks every submission. A condition such as"all": {…}(object instead of a list), a string, or a non-stringfield/opmadesubmit.phpanswer 500 on every submission while the browser ignored it. The server now evaluates such a condition like the browser (ignored), and the definition check (editor, smoketest, selfcheck,submit.php) names it, e.g.fields[3].show_if.all: Expected a list of conditions. An unknownopstill means "equals" on both sides, so working forms are not rejected. - Redirect with
{{field}}: a checkbox value (joined with,) or a missing value (empty) no longer leaves a literal{{tags}}in the URL. \Sinside a character class (e.g.[^\S\r\n], "whitespace but no line break") now matches like the browser on the server too (a no-break space counted as non-space in PHP).maintenance.php deliveries-retry:skipped_oldnow counts only old records that still have an undelivered job; delivered ledgers are no longer reported as "not checked". The result is cached in.delivery/.old-ledgers.json, so repeated runs do not reread every old record.- Nginx rules: the documented
locationblocks use^~prefixes, soconfig/,tests/and thebbf_*.phplibraries stay denied even when the server'slocation ~ \.php$block comes first (a plain regex location lost to it). - Server messages in the respondent's language: "too many requests" (429), "request too large" (413), save failures (500/503), "still being processed" and the payment-start/status 503 answers now come from
lang/*.phpinstead of fixed English text. - Respondent session cookie (
PHPSESSID, CSRF only) is now HttpOnly, Secure on HTTPS and uses strict session IDs. SameSite stays at the browser default so forms embedded in an iframe keep working. - Editor: creating a form with a non-text
nameoridanswers 400 instead of a PHP TypeError 500. - Release workflow: the third-party
setup-phpaction now runs only in a read-only build job; attestation and publishing run in a separate job that uses only GitHub's own actions and re-checksSHA256SUMSbefore signing. - Tests: the editor-preview security test no longer races the iframe load (it failed about 1 in 5 runs on slow machines and could block a release).
- selfcheck / check.php: an
.htaccess.distleft by an earlier release (e.g. an older FTP upgrade) is ignored and named instead of recommending its older, weaker rules; the essential rules are always checked. - Docs: README and docs recommend
--checksumwithSHA256SUMS, say that the code-only ZIP containsCHANGELOG.md, and recommendtools/upgrade.phpfrom the new release for installations on 2.1.0–2.1.2 (their own upgrader is older).