diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index b7c702aee6bc..11074922a5e1 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -30,6 +30,20 @@ */ class Validation implements ValidationInterface { + /** + * Rules whose parameter is a reference to another field name. + * Only for these rules the parameter is looked up in the field rules + * to resolve the label of the referenced field. + * + * @var list + */ + private const FIELD_REFERENCE_RULES = [ + 'matches', + 'differs', + 'required_with', + 'required_without', + ]; + /** * Files to load with validation functions. * @@ -873,7 +887,10 @@ protected function getErrorMessage( $args = [ 'field' => ($label === null || $label === '') ? $field : lang($label), - 'param' => isset($this->rules[$param]['label']) ? lang($this->rules[$param]['label']) : $param, + 'param' => in_array($rule, self::FIELD_REFERENCE_RULES, true) + && isset($this->rules[$param]['label']) + ? lang($this->rules[$param]['label']) + : $param, 'value' => $value ?? '', ]; diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 49baeaba0c79..0c75f4141690 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1858,6 +1858,50 @@ public function testNestedArrayThrowsException(): void ], $this->validation->getErrors()); } + public function testParamIsNotResolvedAsFieldLabelForNonFieldReferenceRules(): void + { + $rules = [ + 'secret' => [ + 'label' => 'Internal Secret Field', + 'rules' => 'required', + ], + 'name' => [ + 'label' => 'Name', + 'rules' => 'required|min_length[secret]', + ], + ]; + + $this->validation->setRules($rules); + + $this->assertFalse($this->validation->run(['name' => 'a', 'secret' => 'x'])); + $this->assertSame( + ['name' => 'The Name field must be at least secret characters in length.'], + $this->validation->getErrors(), + ); + } + + public function testParamResolvesFieldLabelForFieldReferenceRules(): void + { + $rules = [ + 'password' => [ + 'label' => 'Password', + 'rules' => 'required', + ], + 'password_confirmation' => [ + 'label' => 'Password Confirmation', + 'rules' => 'matches[password]', + ], + ]; + + $this->validation->setRules($rules); + + $this->assertFalse($this->validation->run(['password' => 'abc', 'password_confirmation' => 'def'])); + $this->assertSame( + ['password_confirmation' => 'The Password Confirmation field does not match the Password field.'], + $this->validation->getErrors(), + ); + } + public function testRuleWithLeadingAsterisk(): void { $data = [ diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 5fc0cfce5578..083deeeedced 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -59,6 +59,7 @@ Bugs Fixed - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. +- **Validation:** Fixed a bug where a rule parameter that was not a field reference (e.g., a numeric limit) could resolve another field's label and leak it into the error message when it matched a defined field name. Label substitution now only applies to field-referencing rules (``matches``, ``differs``, ``required_with``, ``required_without``). See the repo's `CHANGELOG.md `_