diff --git a/auth-backend/ThrottlesLogins.php b/auth-backend/ThrottlesLogins.php index 6463a50..a6ffb92 100644 --- a/auth-backend/ThrottlesLogins.php +++ b/auth-backend/ThrottlesLogins.php @@ -89,7 +89,7 @@ protected function fireLockoutEvent(Request $request) */ protected function throttleKey(Request $request) { - return Str::lower($request->input($this->username())).'|'.$request->ip(); + return $this->removeSpecialCharacters(Str::lower($request->input($this->username())).'|'.$request->ip()); } /** @@ -121,4 +121,86 @@ public function decayMinutes() { return property_exists($this, 'decayMinutes') ? $this->decayMinutes : 1; } + + /** + * Remove special characters that may allow users to bypass rate limiting. + * + * @param string $key + * @return string + */ + protected function removeSpecialCharacters($key) + { + $values = [ + 'ⓐ' => 'a', + 'ⓑ' => 'b', + 'ⓒ' => 'c', + 'ⓓ' => 'd', + 'ⓔ' => 'e', + 'ⓕ' => 'f', + 'ⓖ' => 'g', + 'ⓗ' => 'h', + 'ⓘ' => 'i', + 'ⓙ' => 'j', + 'ⓚ' => 'k', + 'ⓛ' => 'l', + 'ⓜ' => 'm', + 'ⓝ' => 'n', + 'ⓞ' => 'o', + 'ⓟ' => 'p', + 'ⓠ' => 'q', + 'ⓡ' => 'r', + 'ⓢ' => 's', + 'ⓣ' => 't', + 'ⓤ' => 'u', + 'ⓥ' => 'v', + 'ⓦ' => 'w', + 'ⓧ' => 'x', + 'ⓨ' => 'y', + 'ⓩ' => 'z', + '①' => '1', + '②' => '2', + '③' => '3', + '④' => '4', + '⑤' => '5', + '⑥' => '6', + '⑦' => '7', + '⑧' => '8', + '⑨' => '9', + '⑩' => '10', + '⑪' => '11', + '⑫' => '12', + '⑬' => '13', + '⑭' => '14', + '⑮' => '15', + '⑯' => '16', + '⑰' => '17', + '⑱' => '18', + '⑲' => '19', + '⑳' => '20', + '⓪' => '0', + '⓵' => '1', + '⓶' => '2', + '⓷' => '3', + '⓸' => '4', + '⓹' => '5', + '⓺' => '6', + '⓻' => '7', + '⓼' => '8', + '⓽' => '9', + '⓾' => '10', + '⓫' => '11', + '⓬' => '12', + '⓭' => '13', + '⓮' => '14', + '⓯' => '15', + '⓰' => '16', + '⓱' => '17', + '⓲' => '18', + '⓳' => '19', + '⓴' => '20', + '⓿' => '0', + ]; + + return strtr($key, $values); + } } diff --git a/tests/AuthBackend/ThrottleLoginsTest.php b/tests/AuthBackend/ThrottleLoginsTest.php new file mode 100644 index 0000000..d04adb5 --- /dev/null +++ b/tests/AuthBackend/ThrottleLoginsTest.php @@ -0,0 +1,66 @@ +getMockForTrait(ThrottlesLogins::class); + $reflection = new \ReflectionClass($throttle); + $method = $reflection->getMethod('removeSpecialCharacters'); + $method->setAccessible(true); + + $this->assertSame($expected, $method->invoke($throttle, $value)); + } + + public function specialCharacterProvider(): array + { + return [ + ['ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ', 'abcdefghijklmnopqrstuvwxyz'], + ['⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳', '01234567891011121314151617181920'], + ['⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾', '12345678910'], + ['⓿⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴', '011121314151617181920'], + ['abcdefghijklmnopqrstuvwxyz', 'abcdefghijklmnopqrstuvwxyz'], + ['0123456789', '0123456789'], + ]; + } + + /** + * @test + * @dataProvider emailProvider + */ + public function it_can_generate_throttle_key(string $email, string $expectedEmail): void + { + $throttle = $this->getMockForTrait(ThrottlesLogins::class, [], '', true, true, true, ['username']); + $throttle->method('username')->willReturn('email'); + $reflection = new \ReflectionClass($throttle); + $method = $reflection->getMethod('throttleKey'); + $method->setAccessible(true); + + $request = $this->mock(Request::class); + $request->expects('input')->with('email')->andReturn($email); + $request->expects('ip')->andReturn('192.168.0.1'); + + $this->assertSame($expectedEmail . '|192.168.0.1', $method->invoke($throttle, $request)); + } + + public function emailProvider(): array + { + return [ + 'lowercase special characters' => ['ⓣⓔⓢⓣ@ⓛⓐⓡⓐⓥⓔⓛ.ⓒⓞⓜ', 'test@laravel.com'], + 'uppercase special characters' => ['ⓉⒺⓈⓉ@ⓁⒶⓇⒶⓋⒺⓁ.ⒸⓄⓂ', 'test@laravel.com'], + 'special character numbers' =>['test⑩⓸③@laravel.com', 'test1043@laravel.com'], + 'default email' => ['test@laravel.com', 'test@laravel.com'], + ]; + } +}