Files
opensourcepos/tests/Controllers/ConfigTest.php
T
objecttothisandcoderabbitai[bot] b610ae28ac fix(validation): broaden sendmail path regex, expand i18n, strip advisory IDs
fix(validation): allow Windows sendmail paths, tighten shell metachar exclusions

Broaden PLAIN_FILESYSTEM_PATH_STRICT to accept real-world sendmail formats
while blocking command injection characters not needed in valid paths.

- OSPOSRules.php: allow space, colon, backslash for Windows paths
  (e.g. C:\wamp64\...) and trailing args (-t -i); still excludes
  ampersand, backtick, subshell, redirect, and cmd.exe metacharacters
- OSPOSRulesTest.php: add cases for Windows paths, trailing args, and
  injection payloads
- Remove 7 ConfigTest assertions that expected metacharacter rejection;
  add acceptance test for sendmail path with trailing args

i18n(lang): expand mailpath_invalid message across all locales

- Fill previously empty mailpath_invalid keys across all locales
- Update existing translations (de-CH, de-DE, es-ES, es-MX, fr, nl-BE,
  nl-NL) to reflect newly allowed characters; nl locales corrected from
  English loanwords to proper Dutch terms
- Add missing key to ckb/Config.php

docs: remove security advisory IDs from public-facing files

- AGENTS.md: extend no-advisory-ID rule to documentation and URLs
- INSTALL.md: drop GHSA reference and advisory link from Host Header
  Injection guidance; rationale and fix instructions remain intact

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-09-10 18:01:12 +04:00

344 lines
11 KiB
PHP

<?php
namespace Tests\Controllers;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
class ConfigTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use FeatureTestTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $refresh = false;
protected $namespace = null;
protected function setUp(): void
{
parent::setUp();
}
protected function resetSession(): void
{
$this->withSession(['person_id' => 1, 'menu_group' => 'office']);
}
// ========== Valid Mailpath Tests ==========
public function testValidMailpath_AcceptsStandardPath(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'sendmail',
'mailpath' => '/usr/sbin/sendmail'
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testValidMailpath_AcceptsPathWithDots(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'sendmail',
'mailpath' => '/usr/local/bin/sendmail.local'
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testValidMailpath_AcceptsEmptyStringForNonSendmailProtocol(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'mail',
'mailpath' => ''
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testSendmailProtocol_RequiresMailpath(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'sendmail',
'mailpath' => ''
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('invalid', strtolower($result['message']));
}
public function testNonSendmailProtocol_RejectsMaliciousMailpath(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'smtp',
'mailpath' => '/usr/sbin/sendmail; cat /etc/passwd'
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('invalid', strtolower($result['message']));
}
// ========== Command Injection Prevention Tests ==========
public function testMailpath_RejectsCommandInjection_Semicolon(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'sendmail',
'mailpath' => '/usr/sbin/sendmail; cat /etc/passwd'
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('invalid', strtolower($result['message']));
}
public function testMailpath_AcceptsSendmailPathWithTrailingArgs(): void
{
$this->resetSession();
$response = $this->post('/config/saveEmail', [
'protocol' => 'sendmail',
'mailpath' => '/usr/sbin/sendmail -t -i'
]);
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
// ========== postSaveLocale: payment_reference_code_min / max ==========
private function baseLocalePayload(array $overrides = []): array
{
return array_merge([
'language' => 'en:English',
'currency_symbol' => '$',
'currency_code' => 'USD',
'timezone' => 'UTC',
'dateformat' => 'Y-m-d',
'timeformat' => 'H:i',
'number_locale' => 'en_US',
'currency_decimals' => '2',
'tax_decimals' => '2',
'quantity_decimals' => '2',
'cash_decimals' => '2',
'country_codes' => 'US',
'payment_options_order' => '',
'cash_rounding_code' => '',
'financial_year' => '1',
], $overrides);
}
public function testSaveLocale_AcceptsValidReferenceCodeMinMax(): void
{
$this->resetSession();
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => '3',
'payment_reference_code_max' => '20',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testSaveLocale_AcceptsMinEqualToMax(): void
{
$this->resetSession();
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => '10',
'payment_reference_code_max' => '10',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testSaveLocale_RejectsNonNumericReferenceCodeLimits(): void
{
$this->resetSession();
// Non-numeric values fail integer validation, so the controller returns success===false.
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => 'abc',
'payment_reference_code_max' => 'xyz',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
}
public function testSaveLocale_RejectsZeroReferenceCodeMin(): void
{
$this->resetSession();
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => '0',
'payment_reference_code_max' => '20',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
}
public function testSaveLocale_RejectsNegativeReferenceCodeMin(): void
{
$this->resetSession();
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => '-1',
'payment_reference_code_max' => '20',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
}
public function testSaveLocale_RejectsMaxLessThanMin(): void
{
$this->resetSession();
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
'payment_reference_code_min' => '10',
'payment_reference_code_max' => '5',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
}
// ========== postSaveGeneral: theme validation ==========
private function baseGeneralPayload(array $overrides = []): array
{
return array_merge([
'theme' => 'flatly',
'login_form' => 'floating_labels',
'default_sales_discount_type' => '',
'default_sales_discount' => '0.00',
'default_receivings_discount_type' => '',
'default_receivings_discount' => '0.00',
'enforce_privacy' => '',
'receiving_calculate_average_price' => '',
'lines_per_page' => '20',
'notify_horizontal_position' => 'bottom',
'notify_vertical_position' => 'right',
'image_max_width' => '1000',
'image_max_height' => '1000',
'image_max_size' => '5120',
'image_allowed_types' => ['jpg', 'jpeg', 'gif', 'png'],
'gcaptcha_enable' => '',
'gcaptcha_secret_key' => '',
'gcaptcha_site_key' => '',
'suggestions_first_column' => 'name',
'suggestions_second_column' => '',
'suggestions_third_column' => '',
'giftcard_number' => '',
'derive_sale_quantity' => '',
'multi_pack_enabled' => '',
'include_hsn' => '',
'category_dropdown' => '',
'show_office_group' => '',
], $overrides);
}
public function testSaveGeneral_AcceptsValidTheme(): void
{
$this->resetSession();
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
'theme' => 'darkly',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testSaveGeneral_AcceptsEmptyTheme(): void
{
$this->resetSession();
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
'theme' => '',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertTrue($result['success']);
}
public function testSaveGeneral_RejectsUnknownTheme(): void
{
$this->resetSession();
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
'theme' => 'nonexistent',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('theme', strtolower($result['message']));
}
public function testSaveGeneral_RejectsXssPayloadInTheme(): void
{
$this->resetSession();
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
'theme' => 'x" onerror="alert(document.domain)" x="',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('theme', strtolower($result['message']));
}
public function testSaveGeneral_RejectsNonThemeDirectory(): void
{
$this->resetSession();
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
'theme' => 'fonts',
]));
$response->assertStatus(200);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertStringContainsString('theme', strtolower($result['message']));
}
}