Backups are now AES-256 encrypted zips in three types (full, database
only, master key), catalogued in a new backups table, built by cron rather
than the web request, and kept under uploads/backups with retention in the
nightly job. The encryption key is one value per install held in config.php,
never in the database and never in the file name.
Restore is shared by the setup wizard and the new scripts/restore_cli.php,
which is the only path without an upload size limit. It verifies the key and
unpacks the archive before dropping anything, and dumps the current database
first so a failed import is rolled back.
A backup dumps, zips and encrypts for minutes without issuing a query, so on
a server with a short wait_timeout the connection is closed underneath it and
the UPDATE marking the backup complete is what fails - long after the archive
was written correctly. The connection is now held open for the job and
re-established before any write that follows long file work, including the
database phase of a restore. Retention recovers rows a dropped connection
left behind: still Running after six hours becomes Complete if the archive is
on disk, Failed if it is not.
cron.php's own failure path is hardened to match. It recorded job failures
through the same connection the failing job had just killed, so an uncaught
exception ended the dispatch and no trace of the original error survived.
Failures now also echo to stdout, so cron mails something useful when the
database is unreachable.
Security: the setup wizard's restore step is now closed on any install that
has users, whatever config.php says. $config_enable_setup defaulted to
enabled when the flag was absent, and the flag is only written at the end of
a successful install, so an install abandoned partway left an unauthenticated
endpoint that would drop every table, import an attacker-supplied archive,
and overwrite uploads/ including the .htaccess that stops PHP running there.
Affects 26.07 and earlier. Restoring over a live install is now CLI only.
Statuses can be flagged to pause the resolution clock; sla_history records the intervals a ticket's clock actually ran and the deadline is re-based on remaining budget when it resumes. Adds SLA Summary and SLA by Client reports, an SLA state filter on the ticket list, SLA colouring on kanban cards, and an Urgent column on the Tickets by Client report. DB update 2.5.1. Also fixes resolution SLA verdicts being skipped when resolving via kanban or the client portal.
Response/resolution targets stamped at creation from per-client/priority assignments, business-hours due date math, warn/breach alert stages via cron/ticket_sla.php, ticket list coloring, per-ticket SLA override, admin page for plans/assignments/business hours. DB update 2.5.0. No behavior change unless SLAs are assigned. Bulk reply now only counts Public replies as first response.
generateUserSessionKey() built its AES-128 key with randomString(16), which
draws 12 random bytes and base64url-encodes them into 16 printable characters.
Those 16 characters were handed to openssl_encrypt as the 16-byte key, so the
key that wraps the site master key in the user's session carried 96 bits of
entropy instead of 128. Same for the IV.
The key and IV are now random_bytes(16) and stay raw for the cipher. Base64 is
applied only for transport - the cookie and the session copy of the IV - and
decoded again at the three read sites.
Sessions issued before this change will not decode and are not supported;
anyone logged in at deploy time is prompted to log in again. Nothing is stored
long term in this path, so there is nothing to migrate.
Also guards encryptCredentialEntry against a master key that failed to decrypt.
openssl_encrypt accepts false as a key and silently encrypts under an empty
key, so a session whose cookie had expired could write a credential that no
key would ever recover. It now returns false instead. This was reachable
before this change too, whenever a cookie expired mid-session.
generateUserSessionKey() built its AES-128 key with randomString(16), which
draws 12 random bytes and base64url-encodes them into 16 printable characters.
Those 16 characters were handed to openssl_encrypt as the 16-byte key, so the
key that wraps the site master key in the user's session carried 96 bits of
entropy instead of 128. Same for the IV.
The key and IV are now random_bytes(16) and stay raw for the cipher. Base64 is
applied only for transport - the cookie and the session copy of the IV - and
decoded again at the three read sites.
Sessions issued before this change will not decode and are not supported;
anyone logged in at deploy time is prompted to log in again. Nothing is stored
long term in this path, so there is nothing to migrate.
Also guards encryptCredentialEntry against a master key that failed to decrypt.
openssl_encrypt accepts false as a key and silently encrypts under an empty
key, so a session whose cookie had expired could write a credential that no
key would ever recover. It now returns false instead. This was reachable
before this change too, whenever a cookie expired mid-session.
Return 403 on CSRF failure instead of a 302, which anything following
redirects records as a 200. Set SameSite=Lax at every session_start;
keepalive.php and guest_post.php had no cookie flags at all.
getDnsRecords and getDomainExpirationDate had regressed to shell_exec
(dig/whois) when domain code was moved into functions/domain.php; the
RDAP helper stack was dropped entirely. Restores the dns_get_record +
RDAP + port-43-socket-whois implementation from d1e1609b under the
current function names. getSslCertificate was unaffected.
Replace md5(file_contents)+randomString(2) naming with randomString(32).
No longer reads the file into memory (removes file_get_contents), so
validation is O(1) regardless of size or upload count. Add is_uploaded_file()
and UPLOAD_ERR_OK checks, use pathinfo() for extension extraction, and
return false consistently on all failures (oversize previously returned a
truthy error string that callers treated as a valid filename).