v1.0.0

Security Guide

Clientra CRM is built with security-first principles. This guide documents all security mechanisms in the application and provides best practices for keeping your installation secure.

❌ Critical Security Checklist
  • config/ directory must be outside the web root
  • Delete /install/ directory after setup
  • Never regenerate app_key after data is in the database
  • Never commit config/secrets.php to version control
  • Always serve over HTTPS in production

Authentication & Sessions

Clientra CRM uses PHP session-based authentication. There is no JWT-based authentication for the main web interface (JWT is only used in the API/Webhooks addon).

Session Cookie Settings

php
// Cookie name
session_name('clientra_session');

// Security flags
session_set_cookie_params([
  'lifetime' => 0,          // Session cookie (expires on browser close)
  'path'     => '/',
  'secure'   => true,      // HTTPS only
  'httponly' => true,      // Not accessible via JavaScript
  'samesite' => 'Lax'      // CSRF protection on cross-site requests
]);
FlagValuePurpose
HttpOnlytruePrevents JavaScript from accessing the session cookie, mitigating XSS session theft
SameSiteLaxPrevents the cookie from being sent in cross-site requests initiated by third-party sites
SecuretrueCookie is only transmitted over HTTPS connections
nameclientra_sessionNon-default name makes the CRM less identifiable to scanners

Session Validation

Every API request validates the session before processing:

php
// api.php — All endpoints go through this check
if (!isset($_SESSION['user_id']) || !$_SESSION['user_id']) {
  http_response_code(401);
  echo json_encode([
    'success' => false,
    'message' => 'Unauthorized'
  ]);
  exit();
}

CSRF Protection

Cross-Site Request Forgery (CSRF) protection is implemented using synchronizer tokens. A CSRF token is generated per session and must be included in all state-changing requests.

How It Works

  1. On login, a random 256-bit token is generated and stored in $_SESSION['csrf_token']
  2. The token is returned in API responses for auth endpoints
  3. The frontend ApiClient stores the token and includes it in subsequent POST/PUT requests
  4. The backend validates the token on every state-changing request
php
// Generating a CSRF token
if (!isset($_SESSION['csrf_token'])) {
  $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Validating on POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $token = $data['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
  if (!hash_equals($_SESSION['csrf_token'], $token)) {
    http_response_code(403);
    echo json_encode(['success' => false, 'message' => 'CSRF token invalid']);
    exit();
  }
}
⚠ Include CSRF Token in All POST Requests
The frontend ApiClient automatically includes the CSRF token. If you make direct fetch() calls without using api.call(), you must manually include the token in the request body or as the X-CSRF-Token header.

Role-Based Access Control (RBAC)

Clientra CRM implements a multi-layer RBAC system that controls access at the role, user, module, and feature flag levels.

Role Hierarchy

RoleLevelAccess
admin 1 (highest) Full access to all features including Settings, User Management, Data Wipe, and all admin pages
agent 2 Standard access to sales and operational features. Cannot access Settings, Users, or Permissions.
viewer 3 (lowest) Read-only access to assigned modules. Cannot create, edit, or delete records.

RBAC Layers

Role Permissions

Default permissions for each role stored in role_permissions table. Defines which modules each role can access and what actions (read, create, update, delete) are allowed.

User Permission Overrides

Individual users can have permissions that differ from their role defaults. Stored in user_permissions table. These always take precedence over role permissions.

Module Visibility Control

Control which sidebar modules each user can see via user_module_visibility. Hides navigation items without blocking API access (for UI-level hiding only).

Feature Flags

Edition-specific features (UAE DLD fees, etc.) and experimental features can be toggled globally or per-user via crm_features and user_feature_overrides.

Checking Permissions in PHP

php
// RBACService — checking permissions
$rbac = new RBACService($pdo);

// Check if current user can perform action on module
if (!$rbac->can($_SESSION['user_id'], 'leads', 'delete')) {
  http_response_code(403);
  echo json_encode(['success' => false, 'message' => 'Permission denied']);
  exit();
}

// Check if user has admin role
if (!$rbac->isAdmin($_SESSION['user_id'])) {
  // Restrict to admin-only action
}

Input Validation & SQL Injection Prevention

Validator Service

All user input goes through the Validator service class before processing:

php
$validator = new Validator($data);

$validator->required(['name', 'phone'])
           ->email('email')
           ->maxLength('name', 150)
           ->numeric('price')
           ->in('status', ['New', 'Contacted', 'Won']);

if (!$validator->passes()) {
  echo json_encode([
    'success' => false,
    'message' => 'Validation failed',
    'errors'  => $validator->errors()
  ]);
  exit();
}

Prepared Statements

All database queries use PDO prepared statements — never string concatenation:

php
// CORRECT — Prepared statement
$stmt = $pdo->prepare(
  "SELECT * FROM leads WHERE id = ? AND assigned_to = ?"
);
$stmt->execute([$leadId, $_SESSION['user_id']]);

// WRONG — Never do this (SQL injection risk)
$pdo->query("SELECT * FROM leads WHERE id = $leadId"); // VULNERABLE!

XSS Prevention

ContextMethod
HTML output (PHP)htmlspecialchars($val, ENT_QUOTES, 'UTF-8')
innerHTML in QuickViewModalQuickViewModal.esc(val) — always use this
JSON outputUse json_encode() — automatically escapes HTML
JavaScript stringsAvoid injecting user data into innerHTML without escaping
❌ XSS Risk in QuickViewModal
Any user-generated content injected into innerHTML inside QuickViewModal MUST be escaped using QuickViewModal.esc(value). This is a critical requirement — failing to escape leads to stored XSS vulnerabilities.

File Security

Configuration File Protection

PathProtection Method
config/db.phpOutside web root — not accessible via HTTP
config/secrets.phpOutside web root — not accessible via HTTP
config/sessions/Outside web root, chmod 700
public/.htaccessBlocks direct access to PHP files outside index.php and api.php

Upload Validation

All uploaded files go through validation before being stored:

php
// Allowed MIME types — always validate, never trust file extension alone
$allowedMimes = [
  'image/jpeg', 'image/png', 'image/gif', 'image/webp',
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];

// Validate actual MIME type using finfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);

if (!in_array($mime, $allowedMimes)) {
  throw new Exception('Invalid file type');
}

// Store with random filename — never use original filename
$filename = bin2hex(random_bytes(16)) . '.' . $ext;

Encryption

App Key

The app_key in config/secrets.php is a 256-bit (32-byte) key used for symmetric encryption of sensitive data stored in the database.

❌ Never Regenerate app_key
Email account passwords are encrypted with the app_key using AES-256-CBC encryption. If you regenerate or change the key, all encrypted email passwords become unreadable and all email accounts will stop working. There is no migration path — you would need to re-enter all email passwords.
php
// How email passwords are encrypted (simplified)
function encryptPassword($password, $key) {
  $iv = openssl_random_pseudo_bytes(16);
  $encrypted = openssl_encrypt($password, 'AES-256-CBC', $key, 0, $iv);
  return base64_encode($iv . $encrypted);
}

API Security

Rate Limiting

API requests are rate-limited to prevent brute force and DoS attacks:

Endpoint TypeLimitWindow
Login endpoint10 attemptsper 15 minutes per IP
General API120 requestsper minute per session
File uploads20 filesper minute per session

Security Best Practices

Server Configuration

  • Always use HTTPS — never HTTP in production
  • Keep PHP, MySQL, and OS updated
  • Disable PHP error display in production (display_errors=Off)
  • Enable PHP error logging to a private file
  • Use a web application firewall (WAF)
  • Set expose_php = Off in php.ini

Application Security

  • Delete /install/ immediately after setup
  • Use strong, unique passwords for admin accounts
  • Never share admin credentials between users — create separate accounts
  • Regularly review the Audit Log for suspicious activity
  • Keep regular database backups (Settings → Backup)
  • Use the Trash module to review deleted records

Hardening .htaccess

The included .htaccess applies these protections:

apache
# Disable directory listing
Options -Indexes

# Prevent access to hidden files (.env, .git, etc.)
<FilesMatch "^\.">
  Require all denied
</FilesMatch>

# Only allow GET/POST (no DELETE, PATCH, OPTIONS from browser)
<LimitExcept GET POST>
  Require all denied
</LimitExcept>

# Security headers
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options SAMEORIGIN
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy strict-origin-when-cross-origin

Security Audit Checklist

ItemStatus
/install/ directory deletedRequired
HTTPS enabled with valid certificateRequired
config/ outside web rootRequired
config/secrets.php in .gitignoreRequired
Admin password changed from defaultRequired
PHP display_errors = Off in productionRecommended
Regular database backups configuredRecommended
Activity log reviewed periodicallyRecommended
Session save path has correct permissions (700)Recommended