Skip to content

Security Review - ICoTA Members Plugin

Code review conducted on 2026-02-06

Overview

This document outlines security and code quality concerns found during a review of the ICoTA Members plugin.

Critical Issues

None Found

No critical security vulnerabilities were identified.

Medium Priority Issues

1. Debug Function Exposed via Twig

Location: src/twigextensions/PurchaseExtension.php:141

Issue: The debugUserPurchases() function is exposed to Twig templates and reveals internal system information:

  • Database query results
  • Plugin internals
  • Customer IDs
  • Object structures

Concern:

  • Could leak sensitive information if accidentally used in production templates
  • Exposes internal system structure

Recommendation:

  • Remove from production or wrap in environment check:
php
public function debugUserPurchases(User $user): array
{
    if (!Craft::$app->getConfig()->getGeneral()->devMode) {
        return ['error' => 'Debug functions only available in dev mode'];
    }
    // existing code...
}

Low Priority Issues

2. Duplicate Code in CpController

Location: src/controllers/CpController.php

Issue: The actionIndex() and actionExportCsv() methods contain nearly identical enrichment logic (lines 34-100 and lines 259-325).

Concern:

  • Code duplication makes maintenance difficult
  • Changes need to be applied in two places
  • Potential for bugs if one is updated but not the other

Recommendation: Extract enrichment logic into a private method:

php
private function enrichMemberships(array $memberships): array
{
    // shared enrichment logic
}

3. Unvalidated User Input in Filters

Location: src/controllers/CpController.php:26-28

Issue:

php
$statusFilter = $request->getParam('status');
$chapterFilter = $request->getParam('chapter');
$countryFilter = $request->getParam('country');

Concern:

  • No validation of filter values before use
  • Could potentially be used for injection if not properly escaped in queries

Current Mitigation:

  • Values are only used in array comparisons and in-array checks
  • Not directly used in SQL queries
  • Craft's ORM handles escaping

Recommendation: Add validation for peace of mind:

php
$statusFilter = $request->getParam('status');
if ($statusFilter && !in_array($statusFilter, ['active', 'expired', 'canceled'])) {
    $statusFilter = null;
}

4. JSON Decoding Without Error Handling

Location: Multiple files (CpController.php:60, 285; MemberPurchases.php:134, 154)

Issue:

php
$purchaseData = json_decode($purchase->purchaseData, true);
if ($purchaseData && isset($purchaseData['payment_details']['order_reference'])) {

Concern:

  • If purchaseData contains malformed JSON, json_decode() returns null
  • No error logging for malformed data
  • Silent failures could mask data corruption

Recommendation: Add error handling:

php
$purchaseData = json_decode($purchase->purchaseData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    Craft::error("Failed to decode purchase data: " . json_last_error_msg(), __METHOD__);
    $purchaseData = null;
}

5. Verbose Debug Logging

Location: Multiple locations (Plugin.php, MembershipService.php, PurchaseService.php)

Issue: Extensive use of Craft::info() logging throughout the codebase.

Concern:

  • Logs may grow large in production
  • Some logs contain sensitive data (customer IDs, email addresses)

Current State:

  • Logging is appropriate for debugging
  • Most logs are at INFO level, not ERROR

Recommendation:

  • Review logs before production deployment
  • Consider logging at DEBUG level instead of INFO for verbose details
  • Ensure log rotation is configured

Good Practices Found

✅ Permission Checks

  • MembershipController properly checks editUsers permission (line 28)
  • Requires POST requests for state-changing operations

✅ Input Validation

  • Date validation in MembershipController (lines 74-90)
  • Checks for required fields (line 56)
  • Validates date logic (expiry after start)

✅ CSRF Protection

  • Uses Craft's requirePostRequest() which includes CSRF validation
  • All state-changing operations are POST-only

✅ Error Handling

  • Extensive try-catch blocks
  • Proper error logging
  • Returns JSON responses with success/failure status

✅ SQL Injection Prevention

  • Uses Craft's ORM/Active Record exclusively
  • No raw SQL queries
  • Parameterized queries via Craft's Query Builder

✅ Type Safety

  • Good use of PHP type hints
  • Returns nullable types appropriately
  • Validates element types (e.g., instanceof User)

✅ Stripe Webhook Security

  • Webhooks are processed through Craft Stripe plugin
  • Plugin handles webhook signature verification
  • All webhook events are logged to database

Recommendations Summary

High Priority

  1. Disable or gate debug function with dev mode check

Medium Priority

  1. Extract duplicate enrichment code in CpController
  2. Add input validation for filter parameters

Low Priority

  1. Add JSON decode error handling
  2. Review and reduce verbose logging

Security Best Practices Already Implemented

  • ✅ Uses Craft's permission system
  • ✅ CSRF protection via POST requirements
  • ✅ SQL injection prevention via ORM
  • ✅ No raw file operations
  • ✅ No eval() or similar dangerous functions
  • ✅ Proper error handling and logging
  • ✅ Type-safe parameter handling
  • ✅ XSS prevention via Craft's templating

Conclusion

The plugin is generally well-written with good security practices. The main concerns are:

  1. Code quality: Extract duplicate logic and improve maintainability
  2. Defense in depth: Add extra validation layers even though current code is safe
  3. Production hardening: Disable debug functions in production

Recent Fixes:

  • ✅ CSV export now requires admin access
  • ✅ Removed hardcoded debug function linkUserPurchases()

No critical vulnerabilities were found that would prevent production use, but the remaining recommendations above should be addressed for production readiness.

ICoTA Members Plugin Documentation