resetSession(); // Attempt to change password to 7 characters $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => '1234567' // 7 characters ]); // Assert failure response $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertFalse($result['success'], 'Password with 7 chars should be rejected'); $this->assertEquals(-1, $result['id']); // Verify password was not changed $employee = model(Employee::class); $admin = $employee->get_info(1); $this->assertTrue(password_verify('pointofsale', $admin->password), 'Password should not have been changed'); } /** * Test password validation accepts passwords with exactly 8 characters * * @return void */ public function testPasswordMinLength_Accepts8Characters(): void { $this->resetSession(); // Change password to exactly 8 characters $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => 'pa$$w0rd' // Exactly 8 characters including special chars ]); // Assert success response $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertTrue($result['success'], 'Password with 8 chars should be accepted'); $this->assertEquals(1, $result['id']); // Verify password was changed $employee = model(Employee::class); $admin = $employee->get_info(1); $this->assertTrue(password_verify('pa$$w0rd', $admin->password), 'Password with 8 chars should be accepted'); // Restore original password $employee->change_password([ 'username' => 'admin', 'password' => password_hash('pointofsale', PASSWORD_DEFAULT), 'hash_version' => 2 ], 1); } /** * Test password validation rejects empty password * * @return void */ public function testPasswordMinLength_RejectsEmptyString(): void { $this->resetSession(); // Attempt to set empty password $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => '' // Empty string ]); $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertFalse($result['success'], 'Empty password should be rejected'); $this->assertEquals(-1, $result['id']); } /** * Test password validation rejects whitespace-only passwords * * @return void */ public function testPasswordMinLength_RejectsWhitespaceOnly(): void { $this->resetSession(); // Attempt to set password as only whitespace $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => ' ' // 8 spaces but empty actual password ]); $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertFalse($result['success'], 'Whitespace only password should be rejected'); $this->assertEquals(-1, $result['id']); } /** * Test password validation accepts passwords with special characters * as long as they meet minimum length * * @return void */ public function testPasswordMinLength_AcceptsSpecialCharacters(): void { $this->resetSession(); $specialPassword = 'Str0ng!@#$'; // 11 characters with special chars $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => $specialPassword ]); $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertTrue($result['success'], 'Password with special chars should be accepted'); $this->assertEquals(1, $result['id']); // Verify password works $employee = model(Employee::class); $admin = $employee->get_info(1); $this->assertTrue(password_verify($specialPassword, $admin->password)); // Restore original password $employee->change_password([ 'username' => 'admin', 'password' => password_hash('pointofsale', PASSWORD_DEFAULT), 'hash_version' => 2 ], 1); } /** * Regression test: Verify previous vulnerable behavior is fixed * * Before fix: 1-character passwords like "a" were accepted because * code checked len(hashed_password) which is always 60 for bcrypt * After fix: Raw password is validated before hashing * * @return void */ public function testPasswordMinLength_RejectsPreviousBehavior(): void { $this->resetSession(); // Attempt the previously vulnerable case: single character password $response = $this->post('/home/save', [ 'employee_id' => 1, 'username' => 'admin', 'current_password' => 'pointofsale', 'password' => 'a' // Previously allowed due to bug ]); // This should now fail $response->assertStatus(200); $result = json_decode($response->getJSON(), true); $this->assertFalse($result['success'], 'Single character password should be rejected (CVE fix)'); $this->assertEquals(-1, $result['id']); // Verify password was NOT changed $employee = model(Employee::class); $admin = $employee->get_info(1); $this->assertTrue(password_verify('pointofsale', $admin->password), 'Single character password should be rejected (CVE fix)'); } /** * Helper method to reset session * * @return void */ protected function resetSession(): void { $session = Services::session(); $session->destroy(); $session->set('person_id', 1); // Admin user } }