Every CiviCRM site eventually needs something CiviCRM doesn't do out of the box: a changed form, a workaround for a bug, a Joomla 6 compatibility fix. The quickest way is to edit the file that causes the problem. It is also the way that guarantees the problem comes back. This post is about where fixes should live instead.
Why core edits fail
A CiviCRM upgrade on Joomla installs the new package over the old one. Every file CiviCRM ships – in administrator/components/com_civicrm, in its Joomla plugins, in components/com_civicrm – is replaced. Your edit is gone, silently, and usually nobody remembers it existed until a donation form breaks.
The cost is not the first fix. It is every update after it: either someone re-applies the patch by hand each month, or the site stops getting updates – including security releases.
Where fixes should live
| Kind of change | Put it in |
|---|---|
| Change CiviCRM behavior (validation, form fields, totals, permissions) | A CiviCRM extension using hooks such as hook_civicrm_buildForm, validateForm, post, alterAPIPermissions |
| Change how a CiviCRM page looks | A CiviCRM extension with template overrides or CSS, or the Joomla template |
| New reports, searches, forms | SearchKit and FormBuilder (saved as managed entities in an extension) |
| Joomla-side behavior (user sync, routing, Joomla 6 compatibility) | Your own Joomla plugin, packaged as a separate extension |
| A real bug in CiviCRM | A patch sent upstream, plus a temporary hotfix extension until the release includes it |
CiviCRM itself works this way for urgent problems: for the August 2026 APIv4 security issue, one of the official mitigations was a small "hotfix" extension you could install instead of upgrading. See CiviCRM 6.17.1 security release.
A small example: a hook instead of an edit
Say a contribution form must reject donations below 5 in your currency. Instead of editing the form class, a CiviCRM extension adds a validation hook:
function myorg_civicrm_validateForm($formName, &$fields, &$files, &$form, &$errors) {
if ($formName === 'CRM_Contribute_Form_Contribution_Main') {
$amount = (float) ($fields['total_amount'] ?? 0);
if ($amount > 0 && $amount < 5) {
$errors['total_amount'] = ts('The minimum donation is 5.');
}
}
}
The extension has its own folder, its own version number and its own repository. A CiviCRM upgrade doesn't touch it.
Joomla 6: the same principle
On Joomla 6, CiviCRM's own Joomla plugins need updating for the event system (explained here). Rewriting them in place would be a core edit. Instead, the compatibility code goes in a separate Joomla extension of your own. That is how we run CiviCRM on Joomla 6 on NorCal CarciNET and WARMTH: zero CiviCRM core files changed.
Prove it survives an update
A fix is only "outside core" if an update proves it. On staging:
- Note what each fix does and how to see it working.
- Install a CiviCRM update – or reinstall the current version over itself.
- Check every fix again.
If anything disappears, it was in the wrong place. Move it before the next real update finds it for you.
Keep a register
For every customization, write down: what it does, why, where it lives (extension name and version), and how to test it. One page is enough. It turns "what did the last developer change?" into a five-minute read.
Start the extension with civix
CiviCRM's scaffolding tool civix creates an extension with the right structure, info.xml and hook wiring in a minute, so there is no excuse to put a fix anywhere else:
# terminal – in CiviCRM's extensions directory
civix generate:module org.example.donationmin
cd org.example.donationmin
# add myorg-style hook functions to donationmin.php, then:
cv ext:enable org.example.donationmin --cwd=/var/www/example.org
Keep the extension in its own Git repository and deploy it like any other code. The register of customisations then points at a repository, not at a person's memory.
Inherited a CiviCRM site with core edits? We find them, move them into extensions and hooks, and prove they survive the next update.
CiviCRM Joomla 6 upgrade →
On 5 August 2026 CiviCRM released 6.17.1, 6.16.5 and 6.10.10 (ESR) to fix a highly critical security issue. If your Joomla site runs CiviCRM and has not been updated since, this is the one to do now.
What the problem is
CIVI-SA-2026-37: any user – including an anonymous visitor – could bypass APIv4 permission checks with a specially crafted REST call. In other words, data and actions that should need a login and the right permission could be reached without either.
It is a variation of an earlier issue (CIVI-SA-2026-35), whose first fix was deliberately narrow. The new fix closes more variations of the same bug.
Who is affected
| Affected | CiviCRM 6.17.0 and earlier |
|---|---|
| Fixed in | 6.17.1, 6.16.5, 6.10.10 (ESR), and every later release |
| Risk | Highly critical · access bypass |
The Joomla version doesn't matter – this is in CiviCRM core.
What to do
CiviCRM lists three ways to mitigate it. Any one is enough:
- Upgrade to a fixed version – today, that means the current release (6.19.1 at the time of writing), or 6.10.10+ if you stay on ESR.
- Install the hotfix extension "Hotfix: Camel Attack" through the extensions screen or the command line, if you can't upgrade straight away.
- Apply the patch linked from the advisory – for developers who manage CiviCRM by hand.
Upgrading is the best option; the hotfix buys time. If you use the hotfix, plan the full upgrade anyway.
Check after updating
The advisory notes a regression risk: APIv4 now treats more properties as case-sensitive. That matches how APIv4 is meant to work, but third-party code that relied on the old, looser behavior may break. After updating, test:
- Integrations that call CiviCRM's API – website forms, external apps, sync scripts.
- Custom CiviCRM extensions written in-house.
- FormBuilder forms and SearchKit displays that the public can reach.
An "unknown field" or "not found" error from an integration after this update usually means a property name with the wrong capitalization in the calling code.
Lessons for next time
- Subscribe to CiviCRM's security announcements, so you hear about the next one the day it ships.
- Keep a staging copy fresh enough that a security update can be tested in an hour, not a week. Our update routine is built for exactly this.
- Keep fixes out of CiviCRM core, so security updates never wait on re-applying patches. More here.
Verify it, don't assume it
# terminal – which version is really running, and is the hotfix extension installed?
cv status --cwd=/var/www/example.org | grep -i civicrm
cv ext:list -Li --cwd=/var/www/example.org | grep -i hotfix
# integration code that may rely on case-insensitive APIv4 property names
grep -rnE 'api4\(|Civi\\Api4' /path/to/integrations --include=*.php | cut -c1-140
After a security update, look at the web server log for the days before it: requests to /civicrm/ajax/api4/ or the REST endpoint from unknown addresses, at volumes no form would produce, are worth a closer look.
Behind on CiviCRM updates? We bring CiviCRM up to date on staging, test what takes money and email, and update live.
CiviCRM Joomla 6 upgrade →Page 2 of 2

