perfex-security
Perfex Security Patterns
You are a Perfex CRM security engineer. Your job is to write module code that survives concurrent requests, attacker-controlled inputs, and enumeration attempts — and to enforce the specific patterns (atomic token consume, rate-limited boolean-state endpoints, origin-validated redirects, PII-safe logging) whose absence has caused real production incidents.
Patterns distilled from production Perfex deployments. Each one exists because an absence caused a real incident.
1. Open-redirect prevention
Any endpoint that redirects based on user input must validate the target.
// ❌ WRONG — anyone can craft ?next=https://evil.com
$next = $this->input->get('next');
redirect($next);
// ✅ RIGHT — same-origin only, or a known relative path
$next = $this->input->get('next');
if (!$next || !preg_match('#^/[^/]#', $next)) {
$next = admin_url(); // safe default
}
redirect($next);
Rules:
- Allow only relative paths starting with a single
/. - If you must allow absolute URLs, whitelist against
site_url():if (strpos($next, site_url()) !== 0) $next = site_url(); - Protocol-relative URLs (
//evil.com) are absolute — the check above rejects them via the second char.
2. One-time token consume — race-safe pattern
Tokens (password reset, magic-link login, confirmation links) must be single-use under concurrency.
// ✅ Atomic UPDATE with WHERE used=0, then check affected_rows
public function consume_token($token) {
$this->db->where('token', $token);
$this->db->where('used', 0);
$this->db->where('expires_at >=', date('Y-m-d H:i:s'));
$this->db->update(db_prefix() . 'mymodule_tokens', [
'used' => 1,
'used_at' => date('Y-m-d H:i:s'),
]);
// affected_rows() === 1 proves WE consumed it, not a concurrent request
return $this->db->affected_rows() === 1;
}
Never SELECT-then-UPDATE — that's a TOCTOU race. Two tabs opened simultaneously will both pass the SELECT and both execute the action.
3. Token issuance — don't over-rotate
Issuing a new token should NOT invalidate prior unused ones. Single-use + TTL is sufficient. Rotating invalidates magic links the user already clicked on in their email client, causing support tickets.
public function issue_token($contact_id) {
$token = app_generate_hash(); // Perfex's secure random
$this->db->insert(db_prefix() . 'mymodule_tokens', [
'contact_id' => $contact_id,
'token' => $token,
'expires_at' => date('Y-m-d H:i:s', strtotime('+2 hours')),
'used' => 0,
'created_at' => date('Y-m-d H:i:s'),
]);
return $token;
}
Clean up expired tokens via a cron (app_init + once-per-day flag) rather than on every issue.
4. Rate limit boolean-state endpoints
Any AJAX endpoint that returns yes/no for an attacker-controlled input is an enumeration oracle. Common offenders:
- "Check if email exists" on signup
- "Check if username is taken"
- "Check if coupon is valid"
public function email_exists() {
if (!$this->rate_limit_ok($this->input->ip_address(), 'email_exists', 10, 60)) {
$this->output->set_status_header(429);
return $this->output->set_output(json_encode(['error' => 'Too many requests']));
}
// ... actual check
}
private function rate_limit_ok($key, $bucket, $max, $window_seconds) {
// Implement with tbl<module>_rate_limits or a memory store.
// Reject when count($bucket, $key) in last $window_seconds >= $max.
}
Rule of thumb: 10 attempts per 60s per IP is plenty for legitimate use, painful for enumeration.
5. Cross-module dependencies
Other modules may be uninstalled. Guard with file_exists:
// ❌ fatal error if `billing` module is uninstalled
$this->load->model('billing/billing_model');
// ✅ defensive
$other_model = APPPATH . 'modules/billing/models/Billing_model.php';
if (file_exists($other_model)) {
$this->load->model('billing/billing_model');
$this->billing_model->do_something();
} else {
log_message('info', 'my_module: billing module not installed, skipping');
}
6. PII in logs — never leak
// ❌ NEVER
file_put_contents('/tmp/debug.log', print_r($user, true));
// ❌ Also bad — /tmp survives between requests on some hosts, get rotated nowhere
file_put_contents(APPPATH . 'logs/my_debug.log', $email . "\n");
// ✅ CI's logger respects threshold + rotation
log_message('debug', 'my_module: processed user id=' . $user_id);
Rules:
- Log user IDs, never email/phone/address/DOB.
- Never log passwords, tokens, card numbers, or their hashes.
- Production logs must be readable by ops but not public — check that
application/logs/is behind a deny-from-all.htaccess.
7. target="_blank" links
Every target="_blank" needs rel="noopener noreferrer". No exceptions.
<!-- ❌ reverse-tabnabbing -->
<a href="https://external.com" target="_blank">External</a>
<!-- ✅ -->
<a href="https://external.com" target="_blank" rel="noopener noreferrer">External</a>
Applies to admin and client-area views.
8. CSRF
Perfex has CSRF built in (config/config.php → $config['csrf_protection'] = TRUE). It injects tokens into forms automatically via CI. BUT:
- Raw AJAX requests must include the CSRF token manually: read from
csrf_hash()/get_cookie('csrf_cookie_name'). - Webhook endpoints hit by external services need CSRF excluded. Add to
csrf_exclude_urisinconfig.php— keyed per environment, not globally.
9. Input validation — don't trust client
CI's form validation library is your friend:
$this->form_validation->set_rules('email', 'Email', 'required|valid_email|max_length[191]');
$this->form_validation->set_rules('amount', 'Amount', 'required|numeric|greater_than[0]');
if (!$this->form_validation->run()) {
show_error(validation_errors(), 400);
return;
}
Never $this->input->post('amount') then stuff it into an UPDATE without type-check.
10. HTML output
html_purify() over raw output of user-supplied HTML. htmlspecialchars() (aliased as esc() in some Perfex versions) for text fields in templates.
Related skills
perfex-core-apis—app_generate_hash()for secure random,staff_can()for permission checks, CI's session + CSRF libraries.perfex-database— the atomic UPDATE withaffected_rows() === 1pattern lives there in DDL form.perfex-email— PII-safe logging applies equally to email send attempts; don't log recipient addresses on failure.perfex-theme—target="_blank"+rel="noopener noreferrer"and CSRF exclusions for theme-level form endpoints.
Upstream refs
- Perfex module security (direct-access prevention, path-traversal guards): https://help.perfexcrm.com/module-security/
- OWASP token design: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- CI3 security: https://codeigniter.com/userguide3/libraries/security.html
More from yasserstudio/perfex-crm-skills
perfex-email
Use whenever the user is sending, rendering, or debugging transactional email in a Perfex CRM module — `$this->emails_model->send_simple_email`, `send_mail_template`, email template files under `views/emails/`, admin-recipient fallback chains (`my_module_admin_email` → `contact_form_notification_email` → `smtp_email`), retry queues with exponential backoff stored in `tbl<module>_email_retries`, or cron-driven retry processing via `after_cron_run`. Also trigger when the user says "my Perfex email isn't sending", "send_simple_email returns false", "email failed but the user saw a success page", "SMTP error in my module", "email retry queue", "why didn't my notification email go out", or "email template merge fields". Reinforces the rule that email failure must never break the user flow — always try/catch and enqueue on failure.
1perfex-core-apis
Use whenever the user is working inside a Perfex CRM codebase and touches `get_option`, `update_option`, `add_option`, `delete_option`, `hooks()`, `do_action`, `apply_filters`, `register_activation_hook`, `$this->load`, `get_instance()`, `$CI`, `db_prefix()`, auth helpers like `is_staff_logged_in` / `get_staff_user_id` / `staff_can`, or `_l()`. Also trigger when the user says "my Perfex get_option returns empty", "the hook isn't firing", "how do I hook into Perfex", "module-wide option", "Perfex helper function", "CI loader inside Perfex", or "$CI doesn't work outside a controller". This skill prevents the #1 Perfex bug — silently using `get_option('key', 'default')` which ignores the second argument.
1perfex-theme
Use whenever the user is building or debugging a Perfex CRM custom client-area theme — files under `assets/themes/<theme>/` and `application/views/themes/<theme>/`, asset injection via `app_customers_head`/`app_customers_footer`/`app_admin_head`/`app_admin_footer` hooks, overriding core views, dark mode with `[data-theme="dark"]` plus anti-FOUC `<head>` scripts, RTL/Arabic support, or the jQuery Validate bug where a submit button's `name` is stripped from POST (breaks "Pay Now" / "Save Draft" detection). Also trigger when the user says "my theme CSS is cached after deploy", "Pay Now button loses its value", "jQuery Validate ate my button name", "client area dark mode", "theme file isn't picked up on Linux", or "FOUC when switching themes".
1perfex-database
Use whenever the user writes SQL DDL for a Perfex CRM module, adds a foreign key referencing `tblcontacts`, `tblstaff`, `tblclients`, `tblinvoices`, or any `tbl*` core table, designs `tbl<module>_<entity>` schema, writes `install.php` / `uninstall.php` DDL, writes a migration or `ALTER TABLE`, or debugs "Cannot add foreign key constraint" / "incompatible" errors. Also trigger when the user says "FK won't create in Perfex", "my module's table has wrong collation", "schema in staging differs from prod", "add a column to my Perfex module table", or mentions `db_prefix()` in a DDL context, `utf8mb4_unicode_ci`, or `VARCHAR(191)` vs `VARCHAR(255)`. Prevents the UNSIGNED-INT-vs-signed-INT trap that silently drops foreign-key constraints pointing at Perfex core tables.
1perfex-module-dev
Use whenever the user is creating, modifying, or debugging a Perfex CRM module — anything under `modules/<module_name>/` including `module_name.php`, `install.php`, `uninstall.php`, controllers extending `AdminController` or `ClientsController`, models extending `App_Model`, views, language files, or menu items via `app_menu->add_sidebar_menu_item`. Also trigger when the user says "my Perfex module won't install", "activation hook not running", "the module doesn't show up in Setup", "controller returns 404", "model not loading in Perfex", "admin menu item not showing", or "build a new Perfex module from scratch". Covers module lifecycle, CI3 controller conventions, and the Linux case-sensitivity trap that silently breaks model loading on production.
1perfex-customfields
Use whenever the user is reading, writing, installing, or debugging Perfex CRM custom fields — `tblcustomfields` (definitions), `tblcustomfieldsvalues` (values keyed by `relid`), field types (`input`, `textarea`, `select`, `multiselect`, `checkbox`, `date`, `datetime`, `link`, `colorpicker`, `file`), `fieldto` values (`contacts`, `customers`, `leads`, `invoice`, `estimate`, `contracts`, `tasks`, `tickets`, etc.), `only_admin` visibility, `show_on_client_portal`, `bs_column`, the intentionally-misspelled `disalow_client_to_edit` column, or `render_custom_fields()`. Also trigger when the user says "my custom field isn't showing in the client portal", "I added a custom field in code but it doesn't appear", "custom field value not saving", "only_admin isn't respected", or "Perfex custom field types". Preserves the `disalow_client_to_edit` typo that Perfex core queries by exact name.
1