Skip to the content.

← Back to Development Workflow

Security Best Practices

Last Updated: November 4, 2025
Status: Active

Overview

This document consolidates security best practices, audit findings, and configuration guidelines for the KMP application. It covers both development and production security requirements.

Table of Contents

Security Posture

Current Status: EXCELLENT - All identified vulnerabilities resolved

Based on penetration testing conducted November 3, 2025:

Development Mode (debug=true)

Purpose: Support local development with HTTP, localhost, and IP address access.

Settings:

Use Cases:

Production Mode (debug=false)

Purpose: Maximum security for production deployment.

Settings:

Security Guarantees:

Configuration Files

1. Application.php - CSRF Protection

Location: app/src/Application.php (Lines 406-414)

new CsrfProtectionMiddleware([
    'httponly' => true,    // Always prevent JavaScript access
    'secure' => !Configure::read('debug'),      // false in dev, true in prod
    'sameSite' => Configure::read('debug') ? 'Lax' : 'Strict',
])

2. app.php - Session Configuration

Location: app/config/app.php

'Session' => [
    'defaults' => 'php',
    'timeout' => 30,
    'cookie' => 'PHPSESSID',
    'ini' => [
        'session.cookie_secure' => true,
        'session.cookie_httponly' => true,
        'session.cookie_samesite' => 'Strict',
        'session.use_strict_mode' => true,
    ],
],

Session cookie security attributes (cookie_secure, cookie_samesite) can be overridden in app_local.php per environment. The development container’s app_local.php does not currently override session settings; cookie behavior is toggled via bootstrap.php security validation warnings.

3. bootstrap.php - Security Validation

Location: app/config/bootstrap.php (Lines 100-144)

Runtime security checks warn if production is running with insecure settings.

CSRF Protection

Middleware: CsrfProtectionMiddleware

Features:

Implementation:

// In Application.php middleware stack
->add(new CsrfProtectionMiddleware([
    'httponly' => true,
    'secure' => !Configure::read('debug'),
    'sameSite' => Configure::read('debug') ? 'Lax' : 'Strict',
]))

Token Usage in Forms:

<?= $this->Form->create($entity) ?>
<!-- CSRF token automatically included -->
<?= $this->Form->end() ?>

Security Headers

Content Security Policy (CSP):

Location: app/src/Application.php (Lines 340-390)

$csp = "default-src 'self'; "
     . "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; "
     . "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
     . "font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:; "
     . "img-src 'self' data: https://*.tile.openstreetmap.org https://*.openstreetmap.org; "
     . "connect-src 'self' https://*.tile.openstreetmap.org; "
     . "frame-ancestors 'none'; "
     . "base-uri 'self'; "
     . "form-action 'self';";

// Production only: upgrade insecure requests
if (!$isDevelopment) {
    $csp .= "; upgrade-insecure-requests";
}

$response = $response->withHeader('Content-Security-Policy', $csp);

HTTP Strict Transport Security (HSTS):

// Production only
if (!$isDevelopment) {
    $response = $response->withHeader(
        'Strict-Transport-Security',
        'max-age=86400; includeSubDomains'
    );
}

Other Security Headers:

Session Security

Session Configuration:

'Session' => [
    'defaults' => 'php',
    'timeout' => 30,         // 30 minutes
    'cookie' => 'PHPSESSID',
    'ini' => [
        'session.cookie_secure' => true,      // HTTPS only (in production)
        'session.cookie_httponly' => true,    // Prevent JavaScript access
        'session.cookie_samesite' => 'Strict', // CSRF protection (in production)
        'session.use_strict_mode' => true,    // Validate session IDs
    ],
],

Session Timeout:

Session Regeneration:

Development vs. Production

Environment Configuration

Development (.env):

DEBUG=true

Production (.env):

DEBUG=false

Security Differences

Feature Development Production
Session Cookie Secure false (HTTP allowed) true (HTTPS only)
Session SameSite Lax (Safari compatible) Strict (maximum protection)
CSRF Cookie Secure false true
CSRF SameSite Lax Strict
HSTS Header Not sent Enforced (24 hours)
CSP upgrade-insecure-requests Not included Included
Service Workers Gracefully degrade Full support with HTTPS

Development Benefits

Browser Compatibility:

Network Flexibility:

No HTTPS Required:

Production Guarantees

CSRF Protection:

Session Security:

Transport Security:

Validation:

Penetration Testing Results

Test Overview

Date: November 3, 2025
Scope: HTTP surface security testing
Test Accounts: Various privilege levels (basic user through super user)
Result: All vulnerabilities resolved or verified as false positives

Findings Summary

VULN-001: Mass Assignment (FALSE POSITIVE)

Status: ✅ NOT A VULNERABILITY

Initial Concern: Member entity’s $_accessible array appeared to allow modification of sensitive fields.

Actual Finding: Application properly segregates user-level and admin-level edits:

  1. Separate Controller Actions:
    • partialEdit() - Users editing themselves (manual field assignment)
    • edit() - Administrators with elevated permissions (uses patchEntity)
  2. Authorization Enforcement:
    • Regular users can only partialEdit themselves
    • Policy check: MemberPolicy::canPartialEdit() validates user owns record
    • Admin edit requires canEdit permission
  3. Manual Field Assignment in partialEdit():
    public function partialEdit($id = null) {
        $member = $this->Members->get($id);
        $this->Authorization->authorize($member);
           
        if ($this->request->is(['patch', 'post', 'put'])) {
            // SECURE: Manually sets ONLY allowed fields
            $member->title = $this->request->getData('title');
            $member->sca_name = $this->request->getData('sca_name');
            // ... only safe fields assigned
               
            // Sensitive fields (status, membership_expires_on) are NOT assigned
            // even if attacker submits them
        }
    }
    

Defense in Depth:

VULN-002: Duplicate HTML IDs (FIXED)

Status: ✅ FIXED
Severity: Medium (was accessibility issue, not security)

Issue: Multiple modal elements using same IDs caused accessibility problems.

Fix: Updated modal IDs to be unique per context.

VULN-003: Autocomplete Attributes (FIXED)

Status: ✅ FIXED
Severity: Low (password management UX)

Issue: Missing autocomplete attributes on password fields.

Fix: Added proper autocomplete attributes:

Security Testing Best Practices

Regular Testing:

Automated Security Checks:

# Dependency vulnerability scanning
composer audit

# PHP security checker
./security-checker.sh

Security Checklist

Pre-Deployment

Development Setup

Code Review Security Checks

Controller Actions:

Templates:

Policies:

Database:

Incident Response

If Security Issue Discovered:

  1. Assess Severity:
    • Critical: Immediate fix required
    • High: Fix within 24 hours
    • Medium: Fix in next release
    • Low: Fix as time permits
  2. Document:
    • What the vulnerability is
    • How it was discovered
    • What data/systems are affected
    • What actions have been taken
  3. Fix and Test:
    • Implement fix
    • Add test case to prevent regression
    • Verify fix doesn’t introduce new issues
    • Document fix in code comments
  4. Deploy:
    • Emergency deployment for critical issues
    • Include in next scheduled release for lower severity
    • Update security documentation
  5. Review:
    • Conduct post-mortem
    • Update security practices
    • Add to testing checklist

Public ID System

Overview

The KMP application implements a Public ID system to replace the exposure of internal database IDs to client-side code. This is a critical security and privacy enhancement that prevents information leakage and enumeration attacks.

The Problem: Exposing Internal IDs

Exposing sequential database IDs to clients creates several security problems:

  1. Information Leakage - Sequential IDs reveal record counts and creation order
  2. Enumeration Attacks - Attackers can iterate through all records systematically
  3. Privacy Violations - Deletions and gaps become visible
  4. Predictability - Easy to guess related record IDs

Example Vulnerability:

// Attacker can iterate through all records
for (let id = 1; id < 10000; id++) {
    fetch(`/members/view/${id}`)
}

The Solution: Public IDs

Public IDs are non-sequential, unpredictable identifiers safe to expose to clients:

Internal ID: 123 (sequential, predictable)
Public ID: a7fK9mP2 (random, unpredictable)

Characteristics:

Implementation

PublicIdBehavior provides public ID functionality to any table:

// Add to any Table class
class MembersTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->addBehavior('PublicId');
    }
}

Usage in Controllers:

// Before: Exposes internal ID
public function view($id = null)
{
    $member = $this->Members->get($id);
}

// After: Uses public ID
public function view($publicId = null)
{
    $member = $this->Members->getByPublicId($publicId);
}

Usage in Templates:

// Before
<?= $this->Html->link('View', ['action' => 'view', $member->id]) ?>

// After
<?= $this->Html->link('View', ['action' => 'view', $member->public_id]) ?>

Security Benefits

Before (Vulnerable):

❌ Sequential IDs exposed
❌ Information leakage (count, order, deletions)
❌ Enumeration attacks possible
❌ Predictable identifiers

After (Secure):

✅ Random, non-sequential IDs
✅ No information leakage
✅ Enumeration attacks prevented
✅ Unpredictable identifiers
✅ Same performance (indexed lookups)

Attack Prevention Examples:

  1. Record Enumeration - Attackers cannot systematically discover records
  2. Information Gathering - No way to determine total record count or creation order
  3. Related Record Discovery - Cannot guess related record IDs

Performance

Generating Public IDs

For existing records, use the console command:

# Generate for all tables
bin/cake generate_public_ids --all

# Generate for specific table
bin/cake generate_public_ids members

# Dry run to preview
bin/cake generate_public_ids --all --dry-run

Database Schema

-- Public ID column structure
ALTER TABLE members ADD COLUMN public_id VARCHAR(8) UNIQUE;
CREATE UNIQUE INDEX idx_members_public_id ON members(public_id);

-- Internal relations still use id (foreign keys)
-- Public IDs are only for client-facing references

Migration Strategy

  1. Add Columns - Run migrations (zero downtime)
  2. Generate IDs - Populate existing records
  3. Update Code - Add behavior to tables
  4. Update Controllers - Use public_id instead of id
  5. Update Templates - Use public_id in links
  6. Update JavaScript - Use public_id in AJAX calls

Testing Public IDs

Unit Tests:

public function testPublicIdGeneration()
{
    $member = $this->Members->newEntity(['sca_name' => 'Test']);
    $this->Members->save($member);
    
    $this->assertNotNull($member->public_id);
    $this->assertEquals(8, strlen($member->public_id));
    $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{8}$/', $member->public_id);
}

Integration Tests:

public function testViewWithPublicId()
{
    $member = $this->createMember();
    $this->get('/members/view/' . $member->public_id);
    $this->assertResponseOk();
}

Files

Created:

Plugin Integration:

Testing Security

Manual Security Testing

Authentication:

# Test login
curl -c cookies.txt -d "username=test@example.com&password=wrong" \
  https://example.com/members/login

# Verify session cookie has Secure flag
cat cookies.txt | grep "Secure"

# Test protected endpoint without auth (should fail)
curl -b cookies.txt https://example.com/members/index

Authorization:

CSRF Protection:

# Test POST without CSRF token (should fail)
curl -X POST -b cookies.txt https://example.com/members/add

# Test with invalid CSRF token (should fail)
curl -X POST -b cookies.txt -d "_csrfToken=invalid&..." \
  https://example.com/members/add

Automated Security Scanning

Composer Audit:

cd app
composer audit

Security Checker:

./security-checker.sh

CodeQL (if configured):

# Run CodeQL analysis
codeql database analyze

Session Security Configuration

Session management is critical for protecting user authentication and preventing session hijacking attacks. KMP implements security best practices through configuration.

Session Configuration Overview

KMP’s session configuration in config/app.php:

"Session" => [
    "defaults" => "php",  // Use PHP's default session handling
    "timeout" => 30,      // 30-minute session timeout
    "cookie" => "PHPSESSID",
    "ini" => [
        // Secure cookies require HTTPS
        "session.cookie_secure" => true,
        
        // Prevent JavaScript access (XSS protection)
        "session.cookie_httponly" => true,
        
        // CSRF protection via SameSite policy
        "session.cookie_samesite" => "Strict",
        
        // Validate session IDs for security
        "session.use_strict_mode" => true,
    ],
],

Session Security Features

Feature Purpose Setting
Secure Cookies Require HTTPS session.cookie_secure=true
HttpOnly Flag Prevent XSS access session.cookie_httponly=true
SameSite Policy Prevent CSRF session.cookie_samesite=Strict
Strict Mode Validate IDs session.use_strict_mode=true
Session Timeout Auto-logout 30 minutes

Secure Attribute:

Set-Cookie: PHPSESSID=abc123; Secure; HttpOnly; SameSite=Strict; Path=/

Session Timeout Behavior

30-Minute Timeout:

User Login
  ↓
Session created with TTL = 30 minutes
  ↓
Active use extends timeout
  ↓
30 minutes of inactivity
  ↓
Session expires
  ↓
User redirected to login

Configuration:

To change timeout duration, modify in config/app.php:

"Session" => [
    "timeout" => 60,  // 60-minute timeout
],

Session Storage Options

KMP defaults to PHP file-based sessions. Alternative options available:

1. PHP File Sessions (Default)

"Session" => [
    "defaults" => "php",  // PHP's native file-based sessions
],

Advantages:

Disadvantages:

2. Database Sessions

Enable database-backed sessions:

"Session" => [
    "defaults" => "database",
    "handler" => [
        "engine" => DatabaseSession::class,
        "table" => "sessions",
        "connection" => "default",
    ],
],

Advantages:

Disadvantages:

3. Cache Sessions

Use cache backend for sessions:

"Session" => [
    "defaults" => "cache",
    "handler" => [
        "engine" => CacheSession::class,
        "config" => "default",
    ],
],

Advantages:

Disadvantages:

Development vs. Production Settings

Development Mode (DEBUG=true):

PHP ini settings adjusted for development:

// config/app.php (debug=true)
"Session" => [
    "ini" => [
        "session.cookie_secure" => false,  // Allow HTTP
        "session.cookie_httponly" => true,
        "session.cookie_samesite" => "Lax", // Works with CORS
        "session.use_strict_mode" => true,
    ],
],

Production Mode (DEBUG=false):

// config/app.php (debug=false)
"Session" => [
    "ini" => [
        "session.cookie_secure" => true,   // HTTPS only
        "session.cookie_httponly" => true,
        "session.cookie_samesite" => "Strict", // No CORS
        "session.use_strict_mode" => true,
    ],
],

Session Best Practices

  1. Regenerate After Login
    // In authentication handler
    session_regenerate_id(true);  // Invalidates old session ID
    
  2. Destroy on Logout
    session_destroy();  // Completely clear session
    
  3. Monitor for Hijacking
    // Store user agent in session to detect changes
    if ($_SERVER['HTTP_USER_AGENT'] !== $_SESSION['user_agent']) {
        session_destroy();
        redirect('/login');  // Potential hijack
    }
    
  4. Clear Sensitive Data
    // Don't store passwords or API keys in session
    unset($_SESSION['password']);
    
  5. Use HTTPS
    • Required for Secure cookies
    • Protects session cookies in transit

Encryption and Cryptographic Salt

Cryptographic operations require secure, random salt values for password hashing, token generation, and encryption.

Security Salt Configuration

The security salt is the foundation of password and token security:

"Security" => [
    "salt" => env("SECURITY_SALT"),
],

Generating a Secure Salt

Use a cryptographic random generator:

# Generate a 64-character random salt
php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"

Output Example:

DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgAC7ChtP7k4aYDLG0xVt7z8Fkj4UzP

Salt Requirements

Requirement Detail
Length Minimum 32 characters (64+ recommended)
Randomness Cryptographically random (use generator)
Uniqueness Different for each environment
Secrecy Never committed to version control
Storage In .env file, not in code

Salt Uses

The security salt is used for:

  1. Password Hashing
    $hashedPassword = password_hash($password, PASSWORD_BCRYPT);
    
  2. CSRF Token Generation
    $csrfToken = Security::hash(session_id() . $salt);
    
  3. Encryption Keys
    $encryptionKey = hash('sha256', $salt . 'encryption');
    
  4. Cookie Signing
    Security::hash($cookieData . $salt);
    

Rotating the Salt

When to Rotate:

Rotation Process:

  1. Generate new salt:
    php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"
    
  2. Update .env:
    # Update SECURITY_SALT=new_value
    
  3. Force re-authentication:
    // Invalidate all existing sessions
    bin/cake sessions delete_all
    
  4. Reset user passwords:
    • Send password reset emails
    • Users must set new passwords
  5. Monitor logs:
    tail -f app/logs/error.log
    

Password Hashing

Password hashing uses bcrypt with the security salt:

// Hashing a password
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, [
    'cost' => 12,  // Computational cost (higher = slower/more secure)
    'salt' => $securitySalt,
]);

// Verifying a password
if (password_verify($plainPassword, $hashedPassword)) {
    // Password matches
}

Cost Factor:

For production, use cost 12 or higher.

CSRF Token Security

CSRF tokens prevent cross-site request forgery attacks:

"Security" => [
    "salt" => env("SECURITY_SALT"),
],

// In forms
<?= $this->Form->create() ?>
<?= $this->Form->control('...') ?>
<!-- Automatically includes hidden CSRF token -->
<?= $this->Form->end() ?>

CSRF Token Attributes:

Attribute Value
Name _csrfToken
Expiration Session lifetime
Validation Server-side in middleware
Storage Session + Hidden form field

Cookies can be signed with the security salt to prevent tampering:

// Sign a cookie
$signedValue = Security::hash($unsafeValue . $salt, 'sha256');

// Verify signature
$expectedHash = Security::hash($value . $salt, 'sha256');
if ($signedValue !== $expectedHash) {
    // Cookie was tampered with
}

Environment-Specific Salts

Important: Each environment must have unique salts.

Development (.env):

SECURITY_SALT=dev_salt_not_for_production_change_me

Production (.env):

SECURITY_SALT=cryptographically_random_production_salt_64_chars_here

Compromised development salt doesn’t affect production.


Quick Reference

Development Mode:

# .env
DEBUG=true

# Allows HTTP, works with Safari, IP addresses

Production Mode:

# .env
DEBUG=false

# Requires HTTPS, maximum security

Security Audit:

Key Security Features: