Fix RCE vulnerability + additional security issues#1505
Fix RCE vulnerability + additional security issues#1505
Conversation
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/b4bdcaa4-5c9f-453d-8e39-49f148d053ff Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
📝 WalkthroughWalkthroughThis pull request implements a critical Remote Code Execution (RCE) fix in the template system by replacing dynamic filesystem-derived template whitelists with static hardcoded whitelists, introducing multi-layer validation, adding file existence verification, and implementing directory permission checks. It includes security documentation, code remediation, and a verification script. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
application/modules/guest/controllers/View.php (1)
124-145:⚠️ Potential issue | 🟠 Major
generate_sumex_pdfdoes not validate template parameter.Unlike
generate_invoice_pdf(lines 97-118) andgenerate_quote_pdf(lines 211-228) which validate templates usingvalidate_pdf_template(),generate_sumex_pdfuses the template setting directly without validation at lines 137-139. This could be a security gap.🐛 Proposed fix
if ( ! $invoice_template) { - $invoice_template = get_setting('pdf_invoice_template'); + // Security: Validate PDF template to prevent LFI + $this->load->helper('template'); + $invoice_template = validate_pdf_template(null, 'invoice', 'pdf_invoice_template'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/modules/guest/controllers/View.php` around lines 124 - 145, generate_sumex_pdf fails to validate the incoming $invoice_template before use; fix by checking and sanitizing the template like the other/pdf helpers: if $invoice_template is falsy set it from get_setting('pdf_invoice_template'), then call validate_pdf_template($invoice_template) and if that returns falsy or invalid, fall back to a safe default; ensure the validated template is used (and passed to generate_invoice_sumex or the PDF helper if that function accepts a template) so only validated templates are applied.
🧹 Nitpick comments (4)
SECURITY_ADVISORY_RCE_FIX.md (1)
274-292: Add language identifier to fenced code block.The ASCII diagram code block at line 274 should have a language identifier for consistency and to satisfy markdown linting. Use
textorplaintextfor ASCII diagrams.♻️ Proposed fix
-``` +```text User Input (template name from database) ↓🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@SECURITY_ADVISORY_RCE_FIX.md` around lines 274 - 292, The fenced ASCII diagram code block in SECURITY_ADVISORY_RCE_FIX.md lacks a language identifier; update the block starting with the diagram (the lines containing "User Input (template name from database)" and the arrow diagram) to use a language tag such as text or plaintext (e.g., change ``` to ```text) so the fenced code block has a language identifier and satisfies markdown linting rules.application/modules/invoices/models/Mdl_templates.php (1)
71-82: Consider adding return type hints for consistency.The
check_template_directory_permissions()method at line 115 has a return type hint, butget_invoice_templates()andget_quote_templates()do not. Adding type hints improves code clarity and enables static analysis.♻️ Proposed fix
- public function get_invoice_templates($type = 'pdf') + public function get_invoice_templates(string $type = 'pdf'): array { // Security: Return static whitelist only - NEVER scan filesystem- public function get_quote_templates($type = 'pdf') + public function get_quote_templates(string $type = 'pdf'): array { // Security: Return static whitelist only - NEVER scan filesystemAs per coding guidelines: "Use type hints for all parameters and return types where possible".
Also applies to: 93-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/modules/invoices/models/Mdl_templates.php` around lines 71 - 82, Update the Mdl_templates methods to use explicit type hints: add a string type for the $type parameter and an array return type for get_invoice_templates and get_quote_templates (e.g., public function get_invoice_templates(string $type = 'pdf'): array and same pattern for get_quote_templates) so their signatures match the typed style used in check_template_directory_permissions() and enable static analysis and consistency in Mdl_templates.verify_rce_fix.php (1)
37-61: Comprehensive test cases for path traversal detection.The test cases cover important attack vectors:
- Path traversal (
../)- Standalone
..- Null byte injection
- Absolute paths (Unix)
- Windows drive letters
Consider adding a test for backslash traversal (
..\\) to match the patterns checked invalidate_safe_filename().♻️ Proposed addition
['input' => 'C:\\Windows\\System32', 'should_fail' => true, 'name' => 'Windows drive letter'], + ['input' => '..\\..\\etc\\passwd', 'should_fail' => true, 'name' => 'Path traversal (..\\)'], ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@verify_rce_fix.php` around lines 37 - 61, The test suite is missing a backslash-based traversal case that validate_safe_filename() checks for, so add a test case to $test_cases covering backslash traversal (e.g., input like '..\\' or '..\\\\' as appropriate for PHP string escaping) with should_fail = true and a descriptive name (e.g., 'Backslash traversal (..\\)') so the function's Windows-style/backslash checks are exercised; locate the array of test cases in the snippet and append this entry to ensure validate_safe_filename() is validated against backslash traversal attacks.application/helpers/template_helper.php (1)
163-166: Usesanitize_for_logging()for consistency.Line 164 uses
preg_replace()directly for sanitization, butsanitize_for_logging()is available and used elsewhere in this file. Using the helper ensures consistent sanitization behavior.♻️ Proposed fix
if ($validated === false) { - // Sanitize template name before logging to avoid log injection - $safe_template_name = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $template_name); + $CI = & get_instance(); + $CI->load->helper('file_security'); + $safe_template_name = sanitize_for_logging((string) $template_name); log_message('error', 'Invalid PDF invoice template from settings: ' . $safe_template_name . ', using default');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/template_helper.php` around lines 163 - 166, Replace the ad-hoc preg_replace sanitization for $safe_template_name with the existing sanitize_for_logging() helper to ensure consistent sanitization behavior; update the assignment that currently creates $safe_template_name from preg_replace to call sanitize_for_logging((string) $template_name) and keep the subsequent log_message('error', ...) and return 'InvoicePlane' logic unchanged so logging uses the standardized sanitized value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@application/helpers/template_helper.php`:
- Around line 306-313: The fallback to $default_template must be validated too:
after detect validate_template_name($template_name, $type, $scope) === false,
call validate_template_name($default_template, $type, $scope) and only accept it
if it returns a non-false validated name; if that second validation fails,
sanitize and log both the original and default via sanitize_for_logging and
log_message('error', ...) and then either assign a hardcoded safe template
constant (e.g. a trusted DEFAULT_TEMPLATE) or throw an error to prevent using an
unvalidated value; update references to validate_template_name,
$default_template, sanitize_for_logging, and log_message accordingly.
---
Outside diff comments:
In `@application/modules/guest/controllers/View.php`:
- Around line 124-145: generate_sumex_pdf fails to validate the incoming
$invoice_template before use; fix by checking and sanitizing the template like
the other/pdf helpers: if $invoice_template is falsy set it from
get_setting('pdf_invoice_template'), then call
validate_pdf_template($invoice_template) and if that returns falsy or invalid,
fall back to a safe default; ensure the validated template is used (and passed
to generate_invoice_sumex or the PDF helper if that function accepts a template)
so only validated templates are applied.
---
Nitpick comments:
In `@application/helpers/template_helper.php`:
- Around line 163-166: Replace the ad-hoc preg_replace sanitization for
$safe_template_name with the existing sanitize_for_logging() helper to ensure
consistent sanitization behavior; update the assignment that currently creates
$safe_template_name from preg_replace to call sanitize_for_logging((string)
$template_name) and keep the subsequent log_message('error', ...) and return
'InvoicePlane' logic unchanged so logging uses the standardized sanitized value.
In `@application/modules/invoices/models/Mdl_templates.php`:
- Around line 71-82: Update the Mdl_templates methods to use explicit type
hints: add a string type for the $type parameter and an array return type for
get_invoice_templates and get_quote_templates (e.g., public function
get_invoice_templates(string $type = 'pdf'): array and same pattern for
get_quote_templates) so their signatures match the typed style used in
check_template_directory_permissions() and enable static analysis and
consistency in Mdl_templates.
In `@SECURITY_ADVISORY_RCE_FIX.md`:
- Around line 274-292: The fenced ASCII diagram code block in
SECURITY_ADVISORY_RCE_FIX.md lacks a language identifier; update the block
starting with the diagram (the lines containing "User Input (template name from
database)" and the arrow diagram) to use a language tag such as text or
plaintext (e.g., change ``` to ```text) so the fenced code block has a language
identifier and satisfies markdown linting rules.
In `@verify_rce_fix.php`:
- Around line 37-61: The test suite is missing a backslash-based traversal case
that validate_safe_filename() checks for, so add a test case to $test_cases
covering backslash traversal (e.g., input like '..\\' or '..\\\\' as appropriate
for PHP string escaping) with should_fail = true and a descriptive name (e.g.,
'Backslash traversal (..\\)') so the function's Windows-style/backslash checks
are exercised; locate the array of test cases in the snippet and append this
entry to ensure validate_safe_filename() is validated against backslash
traversal attacks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 711e9d61-5150-4f6f-8d66-c097548e1204
📒 Files selected for processing (7)
CHANGELOG.mdMIGRATION_GUIDE_v1.7.2.mdSECURITY_ADVISORY_RCE_FIX.mdapplication/helpers/template_helper.phpapplication/modules/guest/controllers/View.phpapplication/modules/invoices/models/Mdl_templates.phpverify_rce_fix.php
nielsdrost7
left a comment
There was a problem hiding this comment.
@copilot see if there are other ways to compromise the system and repair them as well
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/61b7e5af-ac8a-453a-b579-9f4d301611b1 Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
Comprehensive security audit completed. Fixed additional vulnerabilities beyond the RCE issue: Vulnerabilities Found & Fixed:
New Security Infrastructure:
Documentation:
Commit: d2a8923 Files Changed: 7 files (+784 lines)
All security fixes validated and documented. |
There was a problem hiding this comment.
Pull request overview
This PR addresses a critical template-based RCE chain in InvoicePlane v1.x by removing runtime filesystem-based template discovery and replacing it with a static allowlist, then layering additional validation and hardening across template selection and several other security-sensitive flows (open redirects, referer parsing, and guest payment query construction).
Changes:
- Replaced dynamic template discovery (
directory_map()-based) with static template allowlists and expanded template validation + safe path construction. - Introduced open-redirect defenses around
HTTP_REFERERhandling and added a reusable security helper for URL validation/escaping and access checks. - Added operator-facing verification and documentation (verification script, advisory, migration guide, changelog).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
application/modules/invoices/models/Mdl_templates.php |
Replaces dynamic template scanning with static allowlists; adds permission audit helper. |
application/helpers/template_helper.php |
Adds multi-layer template validation and centralized validated template path construction. |
application/modules/guest/controllers/View.php |
Switches to centralized validated template path when rendering public invoice/quote views. |
application/helpers/security_helper.php |
Adds helper utilities for safe referers/redirects, URL escaping, IDOR checks, and CSRF verification. |
application/modules/payments/views/modal_add_payment.php |
Replaces direct HTTP_REFERER redirect with validated/escaped safe referer. |
application/modules/custom_fields/controllers/Custom_fields.php |
Replaces direct HTTP_REFERER redirect with validated safe referer. |
application/modules/filter/controllers/Ajax.php |
Adds validation/casting around referer-derived table/ID values for filtering endpoints. |
application/modules/guest/controllers/Payments.php |
Hardens guest payment filtering query by handling empty client lists and casting IDs. |
verify_rce_fix.php |
Adds a CLI verification script to validate presence of the key RCE mitigations. |
SECURITY_ADVISORY_RCE_FIX.md |
Documents the RCE chain and remediation details. |
MIGRATION_GUIDE_v1.7.2.md |
Provides upgrade/audit steps and post-upgrade hardening guidance. |
ADDITIONAL_SECURITY_FIXES_v1.7.2.md |
Documents additional security findings and fixes in v1.7.2. |
CHANGELOG.md |
Adds v1.7.2 release notes summarizing the security changes. |
| * Protects against open redirects, XSS, and other web vulnerabilities. | ||
| */ | ||
|
|
There was a problem hiding this comment.
application/helpers/ip_security_helper.php explicitly notes it is named ip_security to avoid conflicts with CodeIgniter's core security helper (see ip_security_helper.php:22-24). Adding an application-level security_helper.php reintroduces that conflict and can change which helper gets loaded when code calls $this->load->helper('security'), potentially breaking any code expecting CI's built-in security helper functions. Consider renaming this helper (e.g., redirect_security_helper/url_security_helper) and updating call sites accordingly.
| * Protects against open redirects, XSS, and other web vulnerabilities. | |
| */ | |
| * Protects against open redirects, XSS, and other web vulnerabilities. | |
| * | |
| * Compatibility note: | |
| * This application helper shares its name with CodeIgniter's core `security` | |
| * helper. To avoid changing the behavior of `$this->load->helper('security')`, | |
| * load the core helper explicitly before defining the custom helper functions | |
| * in this file. | |
| */ | |
| $core_security_helper = BASEPATH . 'helpers/security_helper.php'; | |
| if (is_file($core_security_helper)) { | |
| require_once $core_security_helper; | |
| } |
| // Get base URL | ||
| $base_url = base_url(); | ||
|
|
||
| // Security: Check if referer starts with base URL (same domain) | ||
| // This prevents open redirect attacks by only allowing internal URLs | ||
| if (str_starts_with($referer, $base_url)) { | ||
| // Additional validation: ensure no control characters or XSS attempts | ||
| if (preg_match('/[\x00-\x1F\x7F<>]/', $referer)) { | ||
| log_message('error', 'Invalid characters in referer URL (hash: ' . hash('sha256', $referer) . ')'); | ||
| return empty($default_url) ? base_url() : $default_url; | ||
| } | ||
|
|
||
| return $referer; | ||
| } |
There was a problem hiding this comment.
get_safe_referer() validates internal URLs via str_starts_with($referer, base_url()). This is fragile if base_url is misconfigured (e.g., missing trailing slash) and can lead to false positives (prefix-matching) or false negatives (scheme/port differences). Safer approach is to parse and compare the host/port of $referer against parse_url(base_url(), ...), and only then allow the redirect (while preserving the path/query).
| // Referer is external or invalid, use safe default | ||
| log_message('debug', 'External referer blocked: ' . parse_url($referer, PHP_URL_HOST)); | ||
| return empty($default_url) ? base_url() : $default_url; |
There was a problem hiding this comment.
The log message for blocked external referers/redirects uses attacker-controlled input (parse_url($referer, PHP_URL_HOST) / parse_url($url, ...)) without sanitizing for logging. Even though it's only the host portion, malformed URLs can still introduce control characters into logs. Please sanitize before logging (e.g., via sanitize_for_logging() from file_security_helper.php) or avoid logging raw parsed values.
| if (!empty($_SERVER['HTTP_REFERER'])) { | ||
| $referer_basename = basename($_SERVER['HTTP_REFERER']); | ||
| // Validate it matches expected pattern (alphanumeric and underscores only) | ||
| if (preg_match('/^[a-zA-Z0-9_]+$/', $referer_basename)) { | ||
| $name = $referer_basename; | ||
| } |
There was a problem hiding this comment.
basename($_SERVER['HTTP_REFERER']) operates on the full URL string, so any query string (e.g. .../table/all?page=2) becomes part of the basename and will fail the new regex, causing $name to fall back to all unexpectedly. Consider extracting the path first (parse_url($referer, PHP_URL_PATH)) and then applying basename() + regex validation.
| // Security: Extract ID from referer URL basename for filtering | ||
| // Using basename() and converting to int prevents injection | ||
| $id = 0; // Default value | ||
| if (!empty($_SERVER['HTTP_REFERER'])) { | ||
| $referer_basename = basename($_SERVER['HTTP_REFERER']); | ||
| // Convert to integer to ensure it's numeric | ||
| $id = (int) $referer_basename; |
There was a problem hiding this comment.
Same issue as above for numeric IDs: basename($_SERVER['HTTP_REFERER']) may include query strings, and (int) casting would then yield 0, potentially breaking filtering. Use parse_url($referer, PHP_URL_PATH) before basename(), and optionally validate that the basename is digits before casting.
| // Security: Extract ID from referer URL basename for filtering | |
| // Using basename() and converting to int prevents injection | |
| $id = 0; // Default value | |
| if (!empty($_SERVER['HTTP_REFERER'])) { | |
| $referer_basename = basename($_SERVER['HTTP_REFERER']); | |
| // Convert to integer to ensure it's numeric | |
| $id = (int) $referer_basename; | |
| // Security: Extract ID from referer URL path basename for filtering | |
| // Parse the URL path first so query strings do not affect basename extraction | |
| $id = 0; // Default value | |
| if (!empty($_SERVER['HTTP_REFERER'])) { | |
| $referer = $_SERVER['HTTP_REFERER']; | |
| $referer_path = parse_url($referer, PHP_URL_PATH); | |
| if (is_string($referer_path) && $referer_path !== '') { | |
| $referer_basename = basename($referer_path); | |
| if (ctype_digit($referer_basename)) { | |
| $id = (int) $referer_basename; | |
| } | |
| } |
| $mdl_templates_content = file_get_contents(APPPATH . 'modules/invoices/models/Mdl_templates.php'); | ||
|
|
||
| if (strpos($mdl_templates_content, 'directory_map') !== false) { | ||
| echo " " . red("✗") . " FAIL: Mdl_templates.php still uses directory_map() - VULNERABLE!\n"; | ||
| $failed++; | ||
| } else { | ||
| echo " " . green("✓") . " Mdl_templates.php does not use directory_map()\n"; | ||
| $passed++; | ||
| } | ||
|
|
||
| if (strpos($mdl_templates_content, 'ALLOWED_INVOICE_TEMPLATES') !== false) { | ||
| echo " " . green("✓") . " Static whitelist ALLOWED_INVOICE_TEMPLATES found\n"; | ||
| $passed++; | ||
| } else { | ||
| echo " " . red("✗") . " FAIL: Static whitelist ALLOWED_INVOICE_TEMPLATES not found - VULNERABLE!\n"; | ||
| $failed++; | ||
| } | ||
|
|
||
| if (strpos($mdl_templates_content, 'ALLOWED_QUOTE_TEMPLATES') !== false) { | ||
| echo " " . green("✓") . " Static whitelist ALLOWED_QUOTE_TEMPLATES found\n"; | ||
| $passed++; | ||
| } else { |
There was a problem hiding this comment.
file_get_contents() can return false (missing file, permissions, etc.). In PHP 8+, passing false into strpos() will throw a TypeError, causing this verification script to crash instead of reporting a clean failure. Add an explicit check for false and print a helpful error before running strpos() checks.
| $mdl_templates_content = file_get_contents(APPPATH . 'modules/invoices/models/Mdl_templates.php'); | |
| if (strpos($mdl_templates_content, 'directory_map') !== false) { | |
| echo " " . red("✗") . " FAIL: Mdl_templates.php still uses directory_map() - VULNERABLE!\n"; | |
| $failed++; | |
| } else { | |
| echo " " . green("✓") . " Mdl_templates.php does not use directory_map()\n"; | |
| $passed++; | |
| } | |
| if (strpos($mdl_templates_content, 'ALLOWED_INVOICE_TEMPLATES') !== false) { | |
| echo " " . green("✓") . " Static whitelist ALLOWED_INVOICE_TEMPLATES found\n"; | |
| $passed++; | |
| } else { | |
| echo " " . red("✗") . " FAIL: Static whitelist ALLOWED_INVOICE_TEMPLATES not found - VULNERABLE!\n"; | |
| $failed++; | |
| } | |
| if (strpos($mdl_templates_content, 'ALLOWED_QUOTE_TEMPLATES') !== false) { | |
| echo " " . green("✓") . " Static whitelist ALLOWED_QUOTE_TEMPLATES found\n"; | |
| $passed++; | |
| } else { | |
| $mdl_templates_path = APPPATH . 'modules/invoices/models/Mdl_templates.php'; | |
| $mdl_templates_content = file_get_contents($mdl_templates_path); | |
| if ($mdl_templates_content === false) { | |
| echo " " . red("✗") . " FAIL: Could not read {$mdl_templates_path}. Check that the file exists and is readable.\n"; | |
| $failed++; | |
| } else { | |
| if (strpos($mdl_templates_content, 'directory_map') !== false) { | |
| echo " " . red("✗") . " FAIL: Mdl_templates.php still uses directory_map() - VULNERABLE!\n"; | |
| $failed++; | |
| } else { | |
| echo " " . green("✓") . " Mdl_templates.php does not use directory_map()\n"; | |
| $passed++; | |
| } | |
| if (strpos($mdl_templates_content, 'ALLOWED_INVOICE_TEMPLATES') !== false) { | |
| echo " " . green("✓") . " Static whitelist ALLOWED_INVOICE_TEMPLATES found\n"; | |
| $passed++; | |
| } else { | |
| echo " " . red("✗") . " FAIL: Static whitelist ALLOWED_INVOICE_TEMPLATES not found - VULNERABLE!\n"; | |
| $failed++; | |
| } | |
| if (strpos($mdl_templates_content, 'ALLOWED_QUOTE_TEMPLATES') !== false) { | |
| echo " " . green("✓") . " Static whitelist ALLOWED_QUOTE_TEMPLATES found\n"; | |
| $passed++; | |
| } else { |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@application/helpers/security_helper.php`:
- Around line 152-183: Replace all loose equality checks and non-strict in_array
calls in this authorization helper with strict comparisons to avoid
type-coercion issues: change occurrences comparing
$CI->session->userdata('user_type') and $invoice->user_id (e.g., places using
==) to ===, change comparisons of $invoice->client_id against client id arrays
to use ===, and update in_array($invoice->client_id, $client_ids) to
in_array($invoice->client_id, $client_ids, true). Ensure every check that used
== or in_array without the strict flag in this helper (including the
guest/admin/regular user branches and comparisons against
session->userdata('user_id')) is updated to use strict operators.
- Around line 247-262: The CSRF check currently allows a bypass when both
$submitted_token and $expected_token are null and lacks timing-safe comparison
and logging sanitization; update the CSRF validation function to enforce
non-empty string tokens (ensure $submitted_token and $expected_token are strings
and not empty) before comparing, use hash_equals($expected_token,
$submitted_token) for the comparison, add a return type hint : bool to the
function signature, sanitize the IP (e.g., via
filter_var($CI->input->ip_address(), FILTER_VALIDATE_IP) or cast/escape before
passing to log_message) when logging mismatches, and add unit tests covering
missing/empty tokens, invalid token, and valid token cases to prevent
regressions.
In `@application/helpers/template_helper.php`:
- Around line 306-336: When falling back to the default template, stop using the
raw $default_template and always reuse the already-validated $validated_default
(or validate it via validate_template_name if not set), run the resolved
filename through validate_file_in_directory() against APPPATH . 'views/' .
$template_dir to ensure it's within the allowed directory before calling
file_exists(), and sanitize any template names in log_message calls with
sanitize_for_logging(); update the branches that rebuild $template_path (the
variables $validated_name, $validated_default, $template_dir, $template_path) so
the fallback path uses the validated value and is validated by
validate_file_in_directory() prior to file_exists(), and replace any raw
$default_template in logs with sanitize_for_logging($validated_default).
In `@application/modules/guest/controllers/Payments.php`:
- Around line 42-44: Replace the raw interpolated WHERE clause in Payments
controller with a Query Builder usage: instead of building
"ip_payments.invoice_id IN (SELECT invoice_id FROM ip_invoices WHERE client_id
IN (...))" via string interpolation on $this->user_clients, use the model/query
builder methods (e.g., $this->mdl_payments and/or $this->mdl_invoices) and
where_in('client_id', $this->user_clients) combined with a proper join or
subquery via the Query Builder so the client IDs are passed as parameters;
locate the existing call to $this->mdl_payments->where(...) and convert it to a
CI3 Query Builder pattern that uses where_in('ip_invoices.client_id',
$this->user_clients) plus join('ip_invoices','ip_invoices.invoice_id =
ip_payments.invoice_id') to achieve the same filtering without raw string
interpolation.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2c137142-a6ca-40a0-b3d2-3deef6288711
📒 Files selected for processing (8)
ADDITIONAL_SECURITY_FIXES_v1.7.2.mdCHANGELOG.mdapplication/helpers/security_helper.phpapplication/helpers/template_helper.phpapplication/modules/custom_fields/controllers/Custom_fields.phpapplication/modules/filter/controllers/Ajax.phpapplication/modules/guest/controllers/Payments.phpapplication/modules/payments/views/modal_add_payment.php
✅ Files skipped from review due to trivial changes (1)
- ADDITIONAL_SECURITY_FIXES_v1.7.2.md
| // Admin users have access to all invoices | ||
| if ($CI->session->userdata('user_type') == 1) { | ||
| return true; | ||
| } | ||
|
|
||
| // Guest users - check if invoice belongs to their assigned clients | ||
| if ($CI->session->userdata('user_type') == 2) { | ||
| $CI->load->model('invoices/mdl_invoices'); | ||
| $CI->load->model('user_clients/mdl_user_clients'); | ||
|
|
||
| $invoice = $CI->mdl_invoices->get_by_id($invoice_id); | ||
| if (!$invoice) { | ||
| return false; | ||
| } | ||
|
|
||
| // Get user's assigned clients | ||
| $user_clients = $CI->mdl_user_clients->assigned_to($CI->session->userdata('user_id'))->get()->result(); | ||
| $client_ids = array_column($user_clients, 'client_id'); | ||
|
|
||
| return in_array($invoice->client_id, $client_ids); | ||
| } | ||
|
|
||
| // Regular users - check if they created the invoice | ||
| $CI->load->model('invoices/mdl_invoices'); | ||
| $invoice = $CI->mdl_invoices->get_by_id($invoice_id); | ||
|
|
||
| if (!$invoice) { | ||
| return false; | ||
| } | ||
|
|
||
| return $invoice->user_id == $CI->session->userdata('user_id'); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "security_helper.php" -type fRepository: InvoicePlane/InvoicePlane
Length of output: 110
🏁 Script executed:
cat -n ./application/helpers/security_helper.phpRepository: InvoicePlane/InvoicePlane
Length of output: 10247
Use strict comparisons in authorization helpers.
Lines 153, 158, 182, 198, 203, and 227 use loose == comparisons, and the in_array() calls on lines 171 and 216 are non-strict. Replace these with === and in_array(..., true) to prevent type coercion vulnerabilities in access control logic.
Also applies to: 197-228
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@application/helpers/security_helper.php` around lines 152 - 183, Replace all
loose equality checks and non-strict in_array calls in this authorization helper
with strict comparisons to avoid type-coercion issues: change occurrences
comparing $CI->session->userdata('user_type') and $invoice->user_id (e.g.,
places using ==) to ===, change comparisons of $invoice->client_id against
client id arrays to use ===, and update in_array($invoice->client_id,
$client_ids) to in_array($invoice->client_id, $client_ids, true). Ensure every
check that used == or in_array without the strict flag in this helper (including
the guest/admin/regular user branches and comparisons against
session->userdata('user_id')) is updated to use strict operators.
| // Get CSRF token from POST data | ||
| $token_name = config_item('csrf_token_name'); | ||
| $submitted_token = $CI->input->post($token_name); | ||
|
|
||
| // Get CSRF token from cookie | ||
| $cookie_name = config_item('csrf_cookie_name'); | ||
| $expected_token = $CI->input->cookie($cookie_name); | ||
|
|
||
| // Compare tokens | ||
| if ($submitted_token === $expected_token) { | ||
| return true; | ||
| } | ||
|
|
||
| // Token mismatch - log and return false | ||
| log_message('error', 'CSRF token mismatch from IP: ' . $CI->input->ip_address()); | ||
| return false; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "security_helper.php" -type f | head -5Repository: InvoicePlane/InvoicePlane
Length of output: 110
🏁 Script executed:
# Get file size to determine if we can read it
wc -l application/helpers/security_helper.php 2>/dev/null || echo "File not found at expected location"Repository: InvoicePlane/InvoicePlane
Length of output: 112
🏁 Script executed:
# Search for security_helper.php in the repository
git ls-files | grep -i security_helperRepository: InvoicePlane/InvoicePlane
Length of output: 196
🏁 Script executed:
cat -n application/helpers/security_helper.php | sed -n '240,263p'Repository: InvoicePlane/InvoicePlane
Length of output: 933
🏁 Script executed:
# Get full context of the CSRF function and any surrounding code
cat -n application/helpers/security_helper.php | sed -n '230,263p'Repository: InvoicePlane/InvoicePlane
Length of output: 1307
🏁 Script executed:
# Check if there are tests for this security function
find . -name "*test*.php" -o -name "*Test.php" | xargs grep -l "verify_csrf_token\|csrf" 2>/dev/null | head -10Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Search for test files more broadly
find . -path "*/tests/*" -name "*.php" -o -path "*/test/*" -name "*.php" 2>/dev/null | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check if sanitize_for_logging function exists
grep -r "sanitize_for_logging" --include="*.php" | head -5Repository: InvoicePlane/InvoicePlane
Length of output: 635
🏁 Script executed:
# Check if hash_equals is used elsewhere in codebase
grep -r "hash_equals" --include="*.php" | head -10Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check the CI framework input class to understand what post() and cookie() return
find . -path "*/system/*" -name "*.php" | xargs grep -l "function post\|function cookie" 2>/dev/null | head -5Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check the sanitize_for_logging function signature and how it's used
cat -n application/helpers/file_security_helper.php | grep -A 5 "function sanitize_for_logging"Repository: InvoicePlane/InvoicePlane
Length of output: 298
🏁 Script executed:
# Check the complete context around the logging in security_helper.php
cat -n application/helpers/security_helper.php | sed -n '235,263p'Repository: InvoicePlane/InvoicePlane
Length of output: 1067
🏁 Script executed:
# Check CodeIgniter's Input class to understand return types of post() and cookie()
find . -name "Input.php" -o -name "input.php" | xargs cat 2>/dev/null | grep -A 10 "function post\|function cookie" | head -40Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check if there are any type hints in the file already
head -50 application/helpers/security_helper.php | tail -40Repository: InvoicePlane/InvoicePlane
Length of output: 1473
🏁 Script executed:
# Check what IP address method returns and if it's safe
cat -n application/helpers/security_helper.php | sed -n '1,50p' | grep -i "use\|require\|include"Repository: InvoicePlane/InvoicePlane
Length of output: 331
🏁 Script executed:
# Look for any CSRF validation tests or usages to understand expected behavior
grep -r "verify_csrf_token" --include="*.php" | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 137
🏁 Script executed:
# Check if the logging sanitization guideline is enforced elsewhere
grep -B 2 -A 2 "log_message.*sanitize_for_logging\|sanitize_for_logging.*log_message" application/helpers/*.phpRepository: InvoicePlane/InvoicePlane
Length of output: 1635
🏁 Script executed:
# Verify the complete function signature and parameters
cat -n application/helpers/security_helper.php | sed -n '228,263p'Repository: InvoicePlane/InvoicePlane
Length of output: 1324
🏁 Script executed:
# Check if hash_equals is available in this codebase's PHP version requirements
grep -r "hash_equals\|php.*version\|require.*php" application/config/*.php 2>/dev/null | head -10Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Verify if the logging data (ip_address()) should be sanitized per guidelines
cat -n application/helpers/ip_security_helper.php | sed -n '1,50p'Repository: InvoicePlane/InvoicePlane
Length of output: 2007
🏁 Script executed:
# Check how other security functions in the codebase handle logging
grep -B 5 -A 1 "log_message" application/helpers/security_helper.phpRepository: InvoicePlane/InvoicePlane
Length of output: 1326
🏁 Script executed:
# Check CodeIgniter's Input class return type for post() and cookie()
find . -type f -name "*.php" | xargs grep -A 5 "public function post\|public function cookie" 2>/dev/null | head -50Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Verify if sanitize_for_logging is properly accessible/loaded
cat -n application/helpers/file_security_helper.php | sed -n '150,165p'Repository: InvoicePlane/InvoicePlane
Length of output: 692
🏁 Script executed:
# Check if there's a test directory and what test structure exists
find . -type d -name "*test*" -o -type d -name "*spec*" 2>/dev/null | head -10Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check PHP version requirements to confirm hash_equals availability
grep -r "php\|version" composer.json .php-version .tool-versions 2>/dev/null | head -10Repository: InvoicePlane/InvoicePlane
Length of output: 527
🏁 Script executed:
# Verify the exact behavior of CodeIgniter's input methods
find . -type f -name "*.php" -path "*/CodeIgniter*" -o -path "*/system/*" 2>/dev/null | xargs grep -A 10 "function post\|function cookie" 2>/dev/null | head -60Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check if ip_address() output needs sanitization in other contexts
grep -B 2 -A 2 "ip_address()" application/helpers/security_helper.phpRepository: InvoicePlane/InvoicePlane
Length of output: 224
🏁 Script executed:
# Review function signatures to verify if return types are added
grep -B 2 "function verify_csrf_token\|function get_safe_referer\|function is_safe_redirect" application/helpers/security_helper.php | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 305
🏁 Script executed:
# Check composer.json for PHP version requirement
cat composer.json | grep -A 5 -B 5 '"php"'Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Verify ip_address() sanitization pattern from other security helpers
grep -A 2 "ip_address()" application/helpers/*.php | grep -A 2 -B 2 log_messageRepository: InvoicePlane/InvoicePlane
Length of output: 294
🏁 Script executed:
# Check if the function has proper return type hints in the codebase
cat -n application/helpers/security_helper.php | sed -n '230,265p'Repository: InvoicePlane/InvoicePlane
Length of output: 1307
🏁 Script executed:
# Verify if CodeIgniter's input->post() and input->cookie() return null when missing
grep -r "function post\|function cookie" . --include="*.php" 2>/dev/null | grep -v node_modules | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 158
🏁 Script executed:
# Double-check if there are any phpunit tests or similar test frameworks configured
ls -la tests/ phpunit.xml test/ .github/workflows/ 2>/dev/null || echo "No standard test location found"Repository: InvoicePlane/InvoicePlane
Length of output: 1341
🏁 Script executed:
# Check if there are phpunit tests for CSRF or security functions
find . -path "*test*" -name "*.php" -type f 2>/dev/null | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check the phpunit.yml configuration
cat .github/workflows/phpunit.ymlRepository: InvoicePlane/InvoicePlane
Length of output: 1408
🏁 Script executed:
# Look for test directories or test files
find . -type f -name "*Test.php" -o -type f -name "*test.php" 2>/dev/null | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Verify CodeIgniter Input class behavior for post() and cookie() methods
cat .env.testing.example 2>/dev/null | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check if this is a Laravel or CodeIgniter project by examining the structure
ls -la | grep -E "artisan|config/app.php|application.php"Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Confirm the exact return behavior of post() and cookie() by checking CI documentation in code
grep -r "get_instance\|CI.*input" application/helpers/security_helper.php | head -5Repository: InvoicePlane/InvoicePlane
Length of output: 234
🏁 Script executed:
# Final verification: Check if sanitize_for_logging() is loaded/available in this context
grep -B 20 "function verify_csrf_token" application/helpers/security_helper.php | grep -i "load\|require\|include"Repository: InvoicePlane/InvoicePlane
Length of output: 111
🏁 Script executed:
# Verify the exact PHP version requirement from composer.json
cat composer.json | head -100 | grep -A 2 -B 2 '"php"\|"require"'Repository: InvoicePlane/InvoicePlane
Length of output: 177
🏁 Script executed:
# Confirm that null === null is indeed the behavior causing the issue
python3 << 'EOF'
# PHP null comparison behavior
print("In PHP 5.x/7.x/8.x:")
print("null === null returns True")
print("null === '' returns False")
print("null === 0 returns False")
print("null === false returns False")
print("\nThis means the check at line 256:")
print("if ($submitted_token === $expected_token)")
print("Will be TRUE when BOTH are null (missing)")
EOFRepository: InvoicePlane/InvoicePlane
Length of output: 314
CSRF validation accepts missing tokens due to null comparison.
At line 256, null === null returns true when both POST field and cookie are absent, allowing CSRF protection to be bypassed. Additionally, the logging at line 261 does not sanitize the ip_address() output (violates "Always sanitize data before logging"), and the function lacks a return type hint (:bool) as required by guidelines.
Validate that tokens are non-empty strings before comparison, use hash_equals() for timing-attack resistance, sanitize log data, and add return type hint:
🛡️ Proposed fix
-function verify_csrf_token()
+function verify_csrf_token(): bool
{
$CI = & get_instance();
// Check if CSRF protection is enabled
if (!config_item('csrf_protection')) {
return true;
}
// Get CSRF token from POST data
$token_name = config_item('csrf_token_name');
$submitted_token = $CI->input->post($token_name);
// Get CSRF token from cookie
$cookie_name = config_item('csrf_cookie_name');
$expected_token = $CI->input->cookie($cookie_name);
- // Compare tokens
- if ($submitted_token === $expected_token) {
- return true;
- }
+ // Validate tokens are non-empty strings
+ if (!is_string($submitted_token) || $submitted_token === ''
+ || !is_string($expected_token) || $expected_token === '') {
+ log_message('error', 'Missing CSRF token from IP: ' . sanitize_for_logging($CI->input->ip_address()));
+ return false;
+ }
+
+ // Use timing-attack-resistant comparison
+ if (hash_equals($expected_token, $submitted_token)) {
+ return true;
+ }
- // Token mismatch - log and return false
- log_message('error', 'CSRF token mismatch from IP: ' . $CI->input->ip_address());
+ // Token mismatch - log and return false
+ log_message('error', 'CSRF token mismatch from IP: ' . sanitize_for_logging($CI->input->ip_address()));
return false;
}Also add test coverage for this critical security function.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@application/helpers/security_helper.php` around lines 247 - 262, The CSRF
check currently allows a bypass when both $submitted_token and $expected_token
are null and lacks timing-safe comparison and logging sanitization; update the
CSRF validation function to enforce non-empty string tokens (ensure
$submitted_token and $expected_token are strings and not empty) before
comparing, use hash_equals($expected_token, $submitted_token) for the
comparison, add a return type hint : bool to the function signature, sanitize
the IP (e.g., via filter_var($CI->input->ip_address(), FILTER_VALIDATE_IP) or
cast/escape before passing to log_message) when logging mismatches, and add unit
tests covering missing/empty tokens, invalid token, and valid token cases to
prevent regressions.
| // Validate the template name | ||
| $validated_name = validate_template_name($template_name, $type, $scope); | ||
| if ($validated_name === false) { | ||
| // Sanitize for logging | ||
| $safe_template_for_log = sanitize_for_logging((string) $template_name); | ||
| log_message('error', 'Invalid template setting: ' . $safe_template_for_log . ', using default: ' . $default_template); | ||
| // Validate default template as well (defense-in-depth) | ||
| $validated_default = validate_template_name($default_template, $type, $scope); | ||
| if ($validated_default === false) { | ||
| log_message('error', 'Critical: Default template also invalid: ' . $default_template); | ||
| show_error('Template system error. Please contact administrator.', 500); | ||
| } | ||
| $validated_name = $validated_default; | ||
| } | ||
|
|
||
| // Construct the template path | ||
| // Security: Both $type and $scope have been validated in layers 3-4 above (lines 242-251) | ||
| // to ensure they only contain the values 'invoice'/'quote' and 'pdf'/'public' respectively. | ||
| // This prevents path traversal attacks through the type/scope parameters. | ||
| $template_dir = $type . '_templates/' . $scope; | ||
| $template_path = APPPATH . 'views/' . $template_dir . '/' . $validated_name . '.php'; | ||
|
|
||
| // Defense-in-depth: Verify template file exists | ||
| if (!file_exists($template_path)) { | ||
| log_message('error', 'Template file not found: ' . sanitize_for_logging($validated_name) . ', using default: ' . $default_template); | ||
| $validated_name = $default_template; | ||
| $template_path = APPPATH . 'views/' . $template_dir . '/' . $validated_name . '.php'; | ||
|
|
||
| // Critical: If even default doesn't exist, throw error | ||
| if (!file_exists($template_path)) { | ||
| log_message('error', 'Critical: Default template file not found for ' . $type . '/' . $scope); |
There was a problem hiding this comment.
Keep the default fallback validated all the way through.
$validated_default is only used when the incoming name fails validation. If the requested template is whitelisted but its file is missing, Line 331 falls back to raw $default_template, logs it unsanitized, and rebuilds the path from that unvalidated value. Reuse one validated/sanitized default for both fallback branches, then run the resolved path through validate_file_in_directory() before file_exists().
🛡️ Proposed hardening
function get_validated_template_path($template_name, $type = 'invoice', $scope = 'public', $default_template = 'InvoicePlane_Web')
{
// Load file_security helper if not already loaded
$CI = & get_instance();
$CI->load->helper('file_security');
+ $safe_default_for_log = sanitize_for_logging((string) $default_template);
+ $validated_default = validate_template_name($default_template, $type, $scope);
+ if ($validated_default === false) {
+ log_message('error', 'Critical: Default template also invalid: ' . $safe_default_for_log);
+ show_error('Template system error. Please contact administrator.', 500);
+ }
// Validate the template name
$validated_name = validate_template_name($template_name, $type, $scope);
if ($validated_name === false) {
// Sanitize for logging
$safe_template_for_log = sanitize_for_logging((string) $template_name);
- log_message('error', 'Invalid template setting: ' . $safe_template_for_log . ', using default: ' . $default_template);
- // Validate default template as well (defense-in-depth)
- $validated_default = validate_template_name($default_template, $type, $scope);
- if ($validated_default === false) {
- log_message('error', 'Critical: Default template also invalid: ' . $default_template);
- show_error('Template system error. Please contact administrator.', 500);
- }
+ log_message('error', 'Invalid template setting: ' . $safe_template_for_log . ', using default: ' . $safe_default_for_log);
$validated_name = $validated_default;
}
@@
// Defense-in-depth: Verify template file exists
if (!file_exists($template_path)) {
- log_message('error', 'Template file not found: ' . sanitize_for_logging($validated_name) . ', using default: ' . $default_template);
- $validated_name = $default_template;
+ log_message('error', 'Template file not found: ' . sanitize_for_logging($validated_name) . ', using default: ' . $safe_default_for_log);
+ $validated_name = $validated_default;
$template_path = APPPATH . 'views/' . $template_dir . '/' . $validated_name . '.php';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@application/helpers/template_helper.php` around lines 306 - 336, When falling
back to the default template, stop using the raw $default_template and always
reuse the already-validated $validated_default (or validate it via
validate_template_name if not set), run the resolved filename through
validate_file_in_directory() against APPPATH . 'views/' . $template_dir to
ensure it's within the allowed directory before calling file_exists(), and
sanitize any template names in log_message calls with sanitize_for_logging();
update the branches that rebuild $template_path (the variables $validated_name,
$validated_default, $template_dir, $template_path) so the fallback path uses the
validated value and is validated by validate_file_in_directory() prior to
file_exists(), and replace any raw $default_template in logs with
sanitize_for_logging($validated_default).
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
application/helpers/security_helper.php (3)
238-263:⚠️ Potential issue | 🔴 CriticalCSRF validation bypassed when both tokens are null; add return type hint and sanitize logging.
When CSRF is enabled but the POST field and cookie are both absent,
null === nullevaluates totrue, allowing the check to pass. Additionally,ip_address()should be sanitized before logging, and the function should have a: boolreturn type hint.🛡️ Proposed fix
-function verify_csrf_token() +function verify_csrf_token(): bool { $CI = & get_instance(); // Check if CSRF protection is enabled if (!config_item('csrf_protection')) { return true; } // Get CSRF token from POST data $token_name = config_item('csrf_token_name'); $submitted_token = $CI->input->post($token_name); // Get CSRF token from cookie $cookie_name = config_item('csrf_cookie_name'); $expected_token = $CI->input->cookie($cookie_name); + // Validate tokens are non-empty strings + if (!is_string($submitted_token) || $submitted_token === '' + || !is_string($expected_token) || $expected_token === '') { + log_message('error', 'Missing CSRF token from IP: ' . sanitize_for_logging($CI->input->ip_address())); + return false; + } + - // Compare tokens - if ($submitted_token === $expected_token) { + // Use timing-attack-resistant comparison + if (hash_equals($expected_token, $submitted_token)) { return true; } // Token mismatch - log and return false - log_message('error', 'CSRF token mismatch from IP: ' . $CI->input->ip_address()); + log_message('error', 'CSRF token mismatch from IP: ' . sanitize_for_logging($CI->input->ip_address())); return false; }As per coding guidelines: "Use type hints for all parameters and return types where possible" and "Always sanitize data before logging using sanitize_for_logging() to prevent log injection."
,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 238 - 263, In verify_csrf_token update the function signature to include a ": bool" return type and change the token comparison to explicitly fail when either the submitted POST token or cookie token is missing (i.e. treat null/empty as invalid rather than allowing null === null to pass); keep the existing strict comparison only when both tokens are non-empty. Also sanitize the IP before logging by passing $CI->input->ip_address() through sanitize_for_logging() in the log_message call so you don't log unsanitized client input. Ensure you reference the function verify_csrf_token, $token_name, $submitted_token, $cookie_name, $expected_token and the log_message usage when making the change.
148-183:⚠️ Potential issue | 🟠 MajorUse strict comparisons in authorization helper to prevent type coercion vulnerabilities.
Lines 153, 158, 171, and 182 use loose comparisons which could allow type coercion to bypass authorization checks. The
in_array()call on line 171 should use strict mode.🛡️ Proposed fix
// Admin users have access to all invoices - if ($CI->session->userdata('user_type') == 1) { + if ((int) $CI->session->userdata('user_type') === 1) { return true; } // Guest users - check if invoice belongs to their assigned clients - if ($CI->session->userdata('user_type') == 2) { + if ((int) $CI->session->userdata('user_type') === 2) { $CI->load->model('invoices/mdl_invoices'); $CI->load->model('user_clients/mdl_user_clients'); $invoice = $CI->mdl_invoices->get_by_id($invoice_id); if (!$invoice) { return false; } // Get user's assigned clients $user_clients = $CI->mdl_user_clients->assigned_to($CI->session->userdata('user_id'))->get()->result(); $client_ids = array_column($user_clients, 'client_id'); - return in_array($invoice->client_id, $client_ids); + return in_array($invoice->client_id, $client_ids, true); } // Regular users - check if they created the invoice $CI->load->model('invoices/mdl_invoices'); $invoice = $CI->mdl_invoices->get_by_id($invoice_id); if (!$invoice) { return false; } - return $invoice->user_id == $CI->session->userdata('user_id'); + return (int) $invoice->user_id === (int) $CI->session->userdata('user_id'); }As per coding guidelines: "Use strict comparison (===, !==) instead of loose comparison."
,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 148 - 183, The authorization helper function user_has_invoice_access uses loose comparisons and in_array without strict checking; update the user type checks (currently comparing $CI->session->userdata('user_type') == 1 and == 2) to strict comparisons (===), change the invoice owner check ($invoice->user_id == $CI->session->userdata('user_id')) to a strict comparison (===) or cast both sides to the same type before comparing, and modify the in_array($invoice->client_id, $client_ids) call to in_array(..., ..., true) to enable strict membership checking; ensure you reference these exact symbols (user_has_invoice_access, $CI->session->userdata('user_type'), $CI->session->userdata('user_id'), in_array) when making the changes.
193-228:⚠️ Potential issue | 🟠 MajorApply same strict comparison fixes to quote access helper.
Same issues as
user_has_invoice_access(): loose comparisons on lines 198, 203, 216, and 227.🛡️ Proposed fix
// Admin users have access to all quotes - if ($CI->session->userdata('user_type') == 1) { + if ((int) $CI->session->userdata('user_type') === 1) { return true; } // Guest users - check if quote belongs to their assigned clients - if ($CI->session->userdata('user_type') == 2) { + if ((int) $CI->session->userdata('user_type') === 2) { $CI->load->model('quotes/mdl_quotes'); $CI->load->model('user_clients/mdl_user_clients'); $quote = $CI->mdl_quotes->get_by_id($quote_id); if (!$quote) { return false; } // Get user's assigned clients $user_clients = $CI->mdl_user_clients->assigned_to($CI->session->userdata('user_id'))->get()->result(); $client_ids = array_column($user_clients, 'client_id'); - return in_array($quote->client_id, $client_ids); + return in_array($quote->client_id, $client_ids, true); } // Regular users - check if they created the quote $CI->load->model('quotes/mdl_quotes'); $quote = $CI->mdl_quotes->get_by_id($quote_id); if (!$quote) { return false; } - return $quote->user_id == $CI->session->userdata('user_id'); + return (int) $quote->user_id === (int) $CI->session->userdata('user_id'); }As per coding guidelines: "Use strict comparison (===, !==) instead of loose comparison."
,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 193 - 228, In function user_has_quote_access, replace loose equality/inequality checks with strict comparisons: use === instead of == when checking $CI->session->userdata('user_type') == 1 and == 2, and use === (or !==) when comparing $quote->user_id to $CI->session->userdata('user_id'); also avoid relying on loose negation for missing quote—use an explicit null/false check (e.g., $quote === null or $quote === false) instead of !$quote; mirror the same strict-comparison changes applied in user_has_invoice_access to the symbols user_has_quote_access, $CI->session->userdata('user_type'), $quote, and $quote->user_id.application/modules/guest/controllers/Payments.php (1)
42-44: 🛠️ Refactor suggestion | 🟠 MajorRaw SQL string interpolation should be replaced with Query Builder methods.
The integer casting via
array_map('intval', ...)is a good defense-in-depth measure. However, the query still uses raw SQL string interpolation which violates the coding guidelines requiring Query Builder usage. Per the context snippet fromapplication/modules/guest/controllers/Invoices.php(lines 54-59), the established pattern in this codebase useswhere_in()with proper Query Builder methods.Consider refactoring to use a join with
where_in():♻️ Suggested refactor using Query Builder
// Security: Use subquery with where_in for safe parameterization - // Previous implementation used string concatenation with implode() which, - // while currently safe because user_clients comes from database, sets a - // dangerous pattern. This uses proper query builder methods. + // Use Query Builder join + where_in for proper parameterization if (empty($this->user_clients)) { // No clients assigned - show no payments $this->mdl_payments->where('1 = 0'); // Always false condition } else { - // Use subquery with proper parameterization - $client_ids_csv = implode(',', array_map('intval', $this->user_clients)); // Sanitize to integers - $this->mdl_payments->where("ip_payments.invoice_id IN (SELECT invoice_id FROM ip_invoices WHERE client_id IN ({$client_ids_csv}))"); + // Use Query Builder with proper parameterization + $this->mdl_payments->join('ip_invoices', 'ip_invoices.invoice_id = ip_payments.invoice_id', 'inner'); + $this->mdl_payments->where_in('ip_invoices.client_id', array_map('intval', $this->user_clients)); }As per coding guidelines: "Use CodeIgniter 3 Query Builder for database queries instead of raw SQL."
,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/modules/guest/controllers/Payments.php` around lines 42 - 44, Replace the raw SQL subquery interpolation in the Payments controller with Query Builder calls: instead of building "ip_payments.invoice_id IN (SELECT ... client_id IN (...))" via string interpolation, use the mdl_payments model and Query Builder methods (e.g., join('ip_invoices', 'ip_invoices.invoice_id = ip_payments.invoice_id') combined with where_in('ip_invoices.client_id', $this->user_clients) or where_in('ip_payments.invoice_id', $invoiceIds) after querying invoice IDs), ensuring you still cast/validate $this->user_clients to integers before passing to where_in(); update the call on $this->mdl_payments (the line that currently uses where("ip_payments.invoice_id IN (SELECT ... )")) to the join + where_in approach.
🧹 Nitpick comments (7)
application/helpers/security_helper.php (6)
130-137: Add return type hint to function signature.♻️ Proposed fix
-function escape_url_for_javascript($url) +function escape_url_for_javascript(string $url): string🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 130 - 137, The function escape_url_for_javascript currently lacks a return type; update its signature to include a string return type (e.g., function escape_url_for_javascript($url): string) and ensure the value returned (result of json_encode(get_safe_referer($url, base_url()), ...)) is always a string (handle json_encode false case if necessary) so callers of escape_url_for_javascript have a correct type guarantee.
193-193: Add parameter and return type hints.♻️ Proposed fix
-function user_has_quote_access($quote_id) +function user_has_quote_access(int $quote_id): bool🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` at line 193, The function user_has_quote_access currently lacks type hints; update its signature to add a parameter type for $quote_id (e.g., int $quote_id or string $quote_id depending on how IDs are used) and a return type of bool, ensure the function body always returns a boolean, and update any docblock/comments and callers if necessary to match the new signature (e.g., change to function user_has_quote_access(int $quote_id): bool).
148-148: Add parameter and return type hints.♻️ Proposed fix
-function user_has_invoice_access($invoice_id) +function user_has_invoice_access(int $invoice_id): bool🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` at line 148, The function user_has_invoice_access lacks parameter and return type hints; update its signature to include a typed parameter (e.g., int $invoice_id) and a bool return type (function user_has_invoice_access(int $invoice_id): bool), update any PHPDoc accordingly, and ensure callers either pass an int or cast/validate the invoice_id before calling so the type contract is preserved.
112-119: Add return type hint to function signature.♻️ Proposed fix
-function escape_url_for_output($url) +function escape_url_for_output(string $url): string🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 112 - 119, The function escape_url_for_output lacks a return type hint; update its signature to declare the correct return type (string) so callers and static analyzers know it returns an escaped string. Modify the function declaration for escape_url_for_output to include ": string" and ensure the function always returns a string (using get_safe_referer and htmlspecialchars as currently implemented) so the type hint is accurate.
37-67: Add return type hint to function signature.Per PSR-12 and coding guidelines, add
: stringreturn type hint.♻️ Proposed fix
-function get_safe_referer($referer = '', $default_url = '') +function get_safe_referer(string $referer = '', string $default_url = ''): string🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 37 - 67, Add a PHP return type declaration to the get_safe_referer function by updating its signature to declare a string return (i.e., add : string to the function declaration for get_safe_referer), and make sure every return path yields a string (cast or ensure base_url(), $default_url and $referer are strings before returning) so the new type hint is satisfied without changing behavior; reference get_safe_referer to locate the signature and the return statements that may need explicit casting.
79-101: Add return type hint to function signature.♻️ Proposed fix
-function validate_redirect_url($url, $default_url = '') +function validate_redirect_url(string $url, string $default_url = ''): string🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@application/helpers/security_helper.php` around lines 79 - 101, The function validate_redirect_url currently lacks a return type hint—update its signature to include a strict return type (string) so callers and static analyzers know it always returns a string; adjust the signature of validate_redirect_url($url, $default_url = '') to add ": string", ensure all return paths already return strings (base_url() and $default_url) and update any docblock above the function if present to match the new return type.CHANGELOG.md (1)
136-136: Minor: Consider simplifying "completely eliminated" to "eliminated".The phrase is slightly redundant per style guidelines.
📝 Optional fix
-- ✅ RCE vulnerability completely eliminated via static whitelist +- ✅ RCE vulnerability eliminated via static whitelist🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CHANGELOG.md` at line 136, The changelog entry uses redundant phrasing "completely eliminated" in the line '✅ RCE vulnerability completely eliminated via static whitelist'; update that string to read '✅ RCE vulnerability eliminated via static whitelist' to conform to style guidelines and remove the word "completely".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@application/helpers/security_helper.php`:
- Around line 65-66: The hostname extracted from parse_url($referer,
PHP_URL_HOST) is logged without sanitization; update the log call in the
External referer block to pass the hostname through sanitize_for_logging()
before concatenating into log_message('debug', ...). Specifically, replace the
direct parse_url($referer, PHP_URL_HOST) usage with
sanitize_for_logging(parse_url($referer, PHP_URL_HOST)) in the log_message call
while leaving the return behavior using empty($default_url) ? base_url() :
$default_url unchanged.
- Around line 99-100: The log call in security_helper.php uses the raw host from
parse_url($url, PHP_URL_HOST) which can enable log injection; update the
log_message call to sanitize the hostname before logging by passing the parsed
host through sanitize_for_logging(), i.e. replace the direct parse_url usage in
the log_message('debug', ...) invocation with a sanitized value; keep the return
behavior (return empty($default_url) ? base_url() : $default_url) unchanged.
---
Duplicate comments:
In `@application/helpers/security_helper.php`:
- Around line 238-263: In verify_csrf_token update the function signature to
include a ": bool" return type and change the token comparison to explicitly
fail when either the submitted POST token or cookie token is missing (i.e. treat
null/empty as invalid rather than allowing null === null to pass); keep the
existing strict comparison only when both tokens are non-empty. Also sanitize
the IP before logging by passing $CI->input->ip_address() through
sanitize_for_logging() in the log_message call so you don't log unsanitized
client input. Ensure you reference the function verify_csrf_token, $token_name,
$submitted_token, $cookie_name, $expected_token and the log_message usage when
making the change.
- Around line 148-183: The authorization helper function user_has_invoice_access
uses loose comparisons and in_array without strict checking; update the user
type checks (currently comparing $CI->session->userdata('user_type') == 1 and ==
2) to strict comparisons (===), change the invoice owner check
($invoice->user_id == $CI->session->userdata('user_id')) to a strict comparison
(===) or cast both sides to the same type before comparing, and modify the
in_array($invoice->client_id, $client_ids) call to in_array(..., ..., true) to
enable strict membership checking; ensure you reference these exact symbols
(user_has_invoice_access, $CI->session->userdata('user_type'),
$CI->session->userdata('user_id'), in_array) when making the changes.
- Around line 193-228: In function user_has_quote_access, replace loose
equality/inequality checks with strict comparisons: use === instead of == when
checking $CI->session->userdata('user_type') == 1 and == 2, and use === (or !==)
when comparing $quote->user_id to $CI->session->userdata('user_id'); also avoid
relying on loose negation for missing quote—use an explicit null/false check
(e.g., $quote === null or $quote === false) instead of !$quote; mirror the same
strict-comparison changes applied in user_has_invoice_access to the symbols
user_has_quote_access, $CI->session->userdata('user_type'), $quote, and
$quote->user_id.
In `@application/modules/guest/controllers/Payments.php`:
- Around line 42-44: Replace the raw SQL subquery interpolation in the Payments
controller with Query Builder calls: instead of building "ip_payments.invoice_id
IN (SELECT ... client_id IN (...))" via string interpolation, use the
mdl_payments model and Query Builder methods (e.g., join('ip_invoices',
'ip_invoices.invoice_id = ip_payments.invoice_id') combined with
where_in('ip_invoices.client_id', $this->user_clients) or
where_in('ip_payments.invoice_id', $invoiceIds) after querying invoice IDs),
ensuring you still cast/validate $this->user_clients to integers before passing
to where_in(); update the call on $this->mdl_payments (the line that currently
uses where("ip_payments.invoice_id IN (SELECT ... )")) to the join + where_in
approach.
---
Nitpick comments:
In `@application/helpers/security_helper.php`:
- Around line 130-137: The function escape_url_for_javascript currently lacks a
return type; update its signature to include a string return type (e.g.,
function escape_url_for_javascript($url): string) and ensure the value returned
(result of json_encode(get_safe_referer($url, base_url()), ...)) is always a
string (handle json_encode false case if necessary) so callers of
escape_url_for_javascript have a correct type guarantee.
- Line 193: The function user_has_quote_access currently lacks type hints;
update its signature to add a parameter type for $quote_id (e.g., int $quote_id
or string $quote_id depending on how IDs are used) and a return type of bool,
ensure the function body always returns a boolean, and update any
docblock/comments and callers if necessary to match the new signature (e.g.,
change to function user_has_quote_access(int $quote_id): bool).
- Line 148: The function user_has_invoice_access lacks parameter and return type
hints; update its signature to include a typed parameter (e.g., int $invoice_id)
and a bool return type (function user_has_invoice_access(int $invoice_id):
bool), update any PHPDoc accordingly, and ensure callers either pass an int or
cast/validate the invoice_id before calling so the type contract is preserved.
- Around line 112-119: The function escape_url_for_output lacks a return type
hint; update its signature to declare the correct return type (string) so
callers and static analyzers know it returns an escaped string. Modify the
function declaration for escape_url_for_output to include ": string" and ensure
the function always returns a string (using get_safe_referer and
htmlspecialchars as currently implemented) so the type hint is accurate.
- Around line 37-67: Add a PHP return type declaration to the get_safe_referer
function by updating its signature to declare a string return (i.e., add :
string to the function declaration for get_safe_referer), and make sure every
return path yields a string (cast or ensure base_url(), $default_url and
$referer are strings before returning) so the new type hint is satisfied without
changing behavior; reference get_safe_referer to locate the signature and the
return statements that may need explicit casting.
- Around line 79-101: The function validate_redirect_url currently lacks a
return type hint—update its signature to include a strict return type (string)
so callers and static analyzers know it always returns a string; adjust the
signature of validate_redirect_url($url, $default_url = '') to add ": string",
ensure all return paths already return strings (base_url() and $default_url) and
update any docblock above the function if present to match the new return type.
In `@CHANGELOG.md`:
- Line 136: The changelog entry uses redundant phrasing "completely eliminated"
in the line '✅ RCE vulnerability completely eliminated via static whitelist';
update that string to read '✅ RCE vulnerability eliminated via static whitelist'
to conform to style guidelines and remove the word "completely".
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d1eea6f2-2582-4651-b68c-57546f2eb655
📒 Files selected for processing (8)
ADDITIONAL_SECURITY_FIXES_v1.7.2.mdCHANGELOG.mdapplication/helpers/security_helper.phpapplication/helpers/template_helper.phpapplication/modules/custom_fields/controllers/Custom_fields.phpapplication/modules/filter/controllers/Ajax.phpapplication/modules/guest/controllers/Payments.phpapplication/modules/payments/views/modal_add_payment.php
✅ Files skipped from review due to trivial changes (1)
- ADDITIONAL_SECURITY_FIXES_v1.7.2.md
🚧 Files skipped from review as they are similar to previous changes (1)
- application/helpers/template_helper.php
| log_message('debug', 'External referer blocked: ' . parse_url($referer, PHP_URL_HOST)); | ||
| return empty($default_url) ? base_url() : $default_url; |
There was a problem hiding this comment.
Sanitize hostname before logging to prevent log injection.
The parse_url($referer, PHP_URL_HOST) output could contain attacker-controlled data. Per coding guidelines, use sanitize_for_logging() before passing to log_message().
🛡️ Proposed fix
// Referer is external or invalid, use safe default
- log_message('debug', 'External referer blocked: ' . parse_url($referer, PHP_URL_HOST));
+ log_message('debug', 'External referer blocked: ' . sanitize_for_logging(parse_url($referer, PHP_URL_HOST) ?? ''));
return empty($default_url) ? base_url() : $default_url;As per coding guidelines: "Always sanitize data before logging using sanitize_for_logging() to prevent log injection."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log_message('debug', 'External referer blocked: ' . parse_url($referer, PHP_URL_HOST)); | |
| return empty($default_url) ? base_url() : $default_url; | |
| log_message('debug', 'External referer blocked: ' . sanitize_for_logging(parse_url($referer, PHP_URL_HOST) ?? '')); | |
| return empty($default_url) ? base_url() : $default_url; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@application/helpers/security_helper.php` around lines 65 - 66, The hostname
extracted from parse_url($referer, PHP_URL_HOST) is logged without sanitization;
update the log call in the External referer block to pass the hostname through
sanitize_for_logging() before concatenating into log_message('debug', ...).
Specifically, replace the direct parse_url($referer, PHP_URL_HOST) usage with
sanitize_for_logging(parse_url($referer, PHP_URL_HOST)) in the log_message call
while leaving the return behavior using empty($default_url) ? base_url() :
$default_url unchanged.
| log_message('debug', 'External redirect URL blocked: ' . parse_url($url, PHP_URL_HOST)); | ||
| return empty($default_url) ? base_url() : $default_url; |
There was a problem hiding this comment.
Sanitize hostname before logging.
Same log injection concern as line 65.
🛡️ Proposed fix
// External URL - reject
- log_message('debug', 'External redirect URL blocked: ' . parse_url($url, PHP_URL_HOST));
+ log_message('debug', 'External redirect URL blocked: ' . sanitize_for_logging(parse_url($url, PHP_URL_HOST) ?? ''));
return empty($default_url) ? base_url() : $default_url;As per coding guidelines: "Always sanitize data before logging using sanitize_for_logging() to prevent log injection."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log_message('debug', 'External redirect URL blocked: ' . parse_url($url, PHP_URL_HOST)); | |
| return empty($default_url) ? base_url() : $default_url; | |
| log_message('debug', 'External redirect URL blocked: ' . sanitize_for_logging(parse_url($url, PHP_URL_HOST) ?? '')); | |
| return empty($default_url) ? base_url() : $default_url; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@application/helpers/security_helper.php` around lines 99 - 100, The log call
in security_helper.php uses the raw host from parse_url($url, PHP_URL_HOST)
which can enable log injection; update the log_message call to sanitize the
hostname before logging by passing the parsed host through
sanitize_for_logging(), i.e. replace the direct parse_url usage in the
log_message('debug', ...) invocation with a sanitized value; keep the return
behavior (return empty($default_url) ? base_url() : $default_url) unchanged.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 3 file(s) based on 4 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 3 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 2 unresolved review comments. A stacked PR containing fixes has been created.
Time taken: |
Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Problem
v1.7.1's LFI patch used
directory_map()to construct template whitelist at runtime. Since template directories were writable, admins could writeevil.php, have it auto-whitelisted, set it via settings, and trigger RCE on any guest invoice view.Additionally, a comprehensive security audit revealed multiple high and medium severity vulnerabilities across the codebase.
Changes
Static whitelist (primary defense - RCE fix)
Replaced filesystem scanning with hardcoded constants in
Mdl_templates.php:Even if
evil.phpexists on disk, it won't be in the whitelist.Multi-layer validation (defense-in-depth)
Enhanced
validate_template_name()intemplate_helper.php:validate_safe_filename()Centralized validation helper
Added
get_validated_template_path()to eliminate duplication inView.php:Additional security fixes
Open Redirect vulnerabilities (CWE-601, HIGH)
Fixed 5 instances of unvalidated
HTTP_REFERERusage that allowed phishing attacks:application/modules/payments/views/modal_add_payment.php- Payment completion redirectapplication/modules/custom_fields/controllers/Custom_fields.php- Custom field deletion redirectapplication/modules/filter/controllers/Ajax.php- 3 AJAX filter operationsSQL query hardening (CWE-89, MEDIUM)
Hardened guest payment queries with explicit integer casting:
application/modules/guest/controllers/Payments.php- Prevents potential SQL injectionHTTP_REFERER validation (CWE-20, MEDIUM)
Added proper validation for HTTP_REFERER usage in AJAX filters:
New security infrastructure
Created
application/helpers/security_helper.phpwith comprehensive security functions:get_safe_referer()- Open redirect preventionvalidate_redirect_url()- Parameter validationescape_url_for_output()- HTML context escapingescape_url_for_javascript()- JavaScript context escapinguser_has_invoice_access()- IDOR prevention for invoicesuser_has_quote_access()- IDOR prevention for quotesverify_csrf_token()- CSRF token validationDocumentation & verification
SECURITY_ADVISORY_RCE_FIX.md- Attack chain, technical details, remediation for RCEADDITIONAL_SECURITY_FIXES_v1.7.2.md- Complete details of additional vulnerabilities and fixesMIGRATION_GUIDE_v1.7.2.md- Upgrade procedures, security hardeningverify_rce_fix.php- 15 automated security testsCHANGELOG.md- v1.7.2 release notes with all security fixesImpact
All exploitation attempts are now logged with security details.
Adding new templates
Requires two steps (by design):
Mdl_templates.phpPrevents runtime filesystem scanning that enabled the vulnerability.
Summary by CodeRabbit
Release Notes
Security
Documentation
Summary by CodeRabbit
Security Fixes
Documentation