Perfex security
Use whenever a Perfex CRM task touches security-sensitive code — issuing or consuming single-use tokens (password reset, magic link, confirmation), race-safe atomic UPDATE with `affected_rows()` check, handling user-controlled redirect URLs (`?next=`, `?redirect=`, `?return_to=`), rate-limiting an AJAX endpoint that leaks boolean state, cross-module model loads, logging PII, adding `target="_blank"` links, or excluding a webhook from CSRF. Also trigger when the user says "my magic link works twice", "password reset is racy", "someone can enumerate users by email", "open redirect in my module", "CSRF blocking my webhook", "rate limit this endpoint", or mentions "TOCTOU", "enumeration", `html_purify`, or `app_generate_hash()`. Every rule here exists because its absence caused a real Perfex production incident.From its SKILL.md
npx -y skills add yasserstudio/perfex-crm-skills --skill perfex-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.9 KB, ~2.0k tokens by cl100k_base, as published. Nobody here has run it
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.
11. Deserialization vulnerability (Perfex 3.4.1 patch)
Perfex 3.4.1 patched a critical unauthenticated remote code execution via insecure deserialization in a bundled third-party library. This affects most earlier releases. If your module accepts serialized input from users (e.g., cached objects, session-stored complex types):
- Never
unserialize()user-controlled input. Usejson_decode()instead. - If you must unserialize, use the
allowed_classesoption (PHP 7.0+):unserialize($data, ['allowed_classes' => false]); - Audit any third-party libraries your module bundles for the same pattern.
12. CSRF exclusion via filter hook
For module-owned webhook endpoints, prefer the filter hook over editing config.php directly:
// In module_name.php — cleaner than manual config.php editing
hooks()->add_filter('csrf_exclude_uris', function ($uris) {
$uris[] = 'my_module_webhook/callback';
return $uris;
});
This is self-contained in your module and removed automatically on deactivation.
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.perfex-payment-gateway— webhook CSRF exclusion and signature verification patterns.
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
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most security skills give in ~2.0k tokens
Counted across 666 of the 889 authors here whose files we hold, read 2026-09-06
- Use parameterized queries for database accessin 82 of 666, across 79 files
- Hash passwords with BCryptin 55 of 666, across 39 files
- Implement rate limiting for public endpointsin 48 of 666, across 34 files
- Use environment variables for secretsin 35 of 666
- Scan dependencies for vulnerabilitiesin 35 of 666, across 24 files
- Validate and sanitize all user inputin 35 of 666, across 32 files
- Add security headers to all responsesin 34 of 666, across 20 files
- Validate all external input at the system boundaryin 26 of 666, across 25 files
- Use parameterized queries to prevent SQL injectionin 25 of 666, across 13 files
- Store secrets in Vault or environment variablesin 25 of 666, across 10 files
- Run containers as a non-root userin 21 of 666, across 18 files
- Validate all input using Bean Validationin 19 of 666, across 5 files
Said here and by no other author read
- use atomic UPDATE with affected_rows check for tokens
- rate limit AJAX endpoints returning boolean state
- guard cross-module model loads with file_exists
- log only user IDs and never PII
- add rel=noopener noreferrer to all target=_blank links
- include CSRF tokens in all manual AJAX requests
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.