Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base: 9e73c4e960
...
compare: 5ba858e118
  • 4 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
33 controllers/components/attempt.php → Controller/Component/AttemptComponent.php
View
@@ -10,36 +10,43 @@
* @package app
* @subpackage app.controllers.components
**/
-class AttemptComponent extends Object {
- public $components = array(
- 'RequestHandler'
- );
-
- // Called before the Controller::beforeFilter().
- // function initialize(&$controller, $options) {
- // }
+class AttemptComponent extends Component {
// Called after the Controller::beforeFilter() and before the controller action
public function startup($controller) {
- $this->controller = $controller;
+ $this->Controller = $controller;
$this->Attempt = ClassRegistry::init('Attempt.Attempt');
}
public function count($action) {
- return $this->Attempt->count($this->RequestHandler->getClientIP(), $action);
+ return $this->Attempt->count(
+ $this->Controller->request->clientIp(),
+ $action
+ );
}
public function limit($action, $limit = 5) {
- return $this->Attempt->limit($this->RequestHandler->getClientIP(), $action, $limit);
+ return $this->Attempt->limit(
+ $this->Controller->request->clientIp(),
+ $action,
+ $limit
+ );
}
public function fail($action, $duration = '+10 minutes') {
- return $this->Attempt->fail($this->RequestHandler->getClientIP(), $action, $duration);
+ return $this->Attempt->fail(
+ $this->Controller->request->clientIp(),
+ $action,
+ $duration
+ );
}
public function reset($action) {
- return $this->Attempt->reset($this->RequestHandler->getClientIP(), $action);
+ return $this->Attempt->reset(
+ $this->Controller->request->clientIp(),
+ $action
+ );
}
public function cleanup() {
27 models/attempt.php → Model/Attempt.php
View
@@ -18,24 +18,27 @@ public function limit($ip, $action, $limit) {
}
public function fail($ip, $action, $duration) {
- $this->create(array(
- 'ip' => $ip,
- 'action' => $action,
- 'expires' => date('Y-m-d H:i:s', strtotime($duration))
- ));
+ $this->create(
+ array(
+ 'ip' => $ip,
+ 'action' => $action,
+ 'expires' => date('Y-m-d H:i:s', strtotime($duration))
+ )
+ );
return $this->save();
}
public function reset($ip, $action) {
- return $this->deleteAll(array(
- 'ip' => $ip,
- 'action' => $action
- ),false, false);
+ return $this->deleteAll(
+ array('ip' => $ip, 'action' => $action),
+ false
+ );
}
public function cleanup() {
- return $this->deleteAll(array(
- 'expires <' => date('Y-m-d H:i:s')
- ), false, false);
+ return $this->deleteAll(
+ array('expires <' => date('Y-m-d H:i:s')),
+ false
+ );
}
}
143 README.markdown
View
@@ -1,8 +1,7 @@
Attempt Component
=================
-A simple component to protect sensitive actions from brute force attacks.
-
+A CakePHP 2.0 or 2.1 plugin that helps to protect sensitive actions from brute force attacks.
API
---
@@ -26,56 +25,100 @@ Deletes all expired failed attempts from the database. This should be run via Ca
Schema
------
- CREATE TABLE `attempts` (
- `id` char(36) NOT NULL DEFAULT '',
- `ip` varchar(64) DEFAULT NULL,
- `action` varchar(32) DEFAULT NULL,
- `created` datetime DEFAULT NULL,
- `expires` datetime DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `ip` (`ip`,`action`),
- KEY `expires` (`expires`)
- ) ENGINE=MyISAM DEFAULT CHARSET=utf8
+ CREATE TABLE `attempts` (
+ `id` char(36) NOT NULL DEFAULT '',
+ `ip` varchar(64) DEFAULT NULL,
+ `action` varchar(32) DEFAULT NULL,
+ `created` datetime DEFAULT NULL,
+ `expires` datetime DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `ip` (`ip`,`action`),
+ KEY `expires` (`expires`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8
Example Implementation
----------------------
+
+ <?php
+ class ExampleController extends AppController {
+
+ public $components = array(
+ 'Attempt.Attempt'
+ );
+
+ public $loginAttemptLimit = 10;
+ public $loginAttemptDuration = '+1 hour';
+
+ public function login() {
+ // Form submitted?
+ if ( $formSubmitted = true ) {
+ // All required fields entered?
+ if ( $validFormData = true ) {
+ // Limit to 10 failed attempts
+ if ( $this->Attempt->limit('login', $this->loginAttemptLimit) ) {
+ // Validate user credentials
+ if ( $validCredentials = true ) {
+ // Log user in
+ } else {
+ // Invalid credentials, count as failed attempt for an hour
+ $this->Attempt->fail('login', $this->loginAttemptDuration);
+ $this->Session->setFlash('Unknown user or wrong password');
+ }
+ } else {
+ // User exceeded attempt limit
+ // Ideally show a CAPTCHA (ensuring this is not a robot
+ // without blocking out and frustrating users),
+ // otherwise show error message
+ $this->Session->setFlash('Too many failed attempts!');
+ }
+ } else {
+ // Invalid form data but keep it ambiguous
+ $this->Session->setFlash('Unknown user or wrong password');
+ }
+ }
+ }
+ }
+
+
+Alternate Implementation (simple admin login)
+---------------------------------------------
+
+ <?php
+ class UsersController extends AppController {
+
+ public $components = array(
+ 'Security',
+ 'Attempt.Attempt'
+ );
+
+ public $loginAttemptLimit = 10;
+ public $loginAttemptDuration = '+1 hour';
- class ExampleController extends Controller {
-
- var $components = array(
- 'Attempt'
- );
-
- var $loginAttemptLimit = 10;
- var $loginAttemptDuration = '+1 hour';
-
- public function login() {
- // Form submitted?
- if ( $formSubmitted = true ) {
- // All required fields entered?
- if ( $validFormData = true ) {
- // Limit to 10 failed attempts
- if ( $this->Attempt->limit('login', $this->loginAttemptLimit) ) {
- // Validate user credentials
- if ( $validCredentials = true ) {
- // Log user in
- } else {
- // Invalid credentials, count as failed attempt for an hour
- $this->Attempt->fail('login', $this->loginAttemptDuration);
- $this->Session->setFlash('Unknown user or wrong password');
- }
- } else {
- // User exceeded attempt limit
- // Ideally show a CAPTCHA (ensuring this is not a robot
- // without blocking out and frustrating users),
- // otherwise show error message
- $this->Session->setFlash('Too many failed attempts!');
- }
- } else {
- // Invalid form data but keep it ambiguous
- $this->Session->setFlash('Unknown user or wrong password');
- }
- }
- }
- }
+ public function admin_login() {
+ if (empty($this->data)) {
+ return;
+ }
+ // check for repeated login attempts
+ if ($this->Attempt->limit('admin_login', $this->loginAttemptLimit)) {
+ if ($this->request->is('post')) {
+ if ($this->Auth->login()) {
+ // login was successful, redirect to admin menu
+ $this->redirect(array(
+ 'controller' => 'users',
+ 'action' => 'index',
+ 'admin' => true
+ ));
+ } else {
+ // increment the attempt counter
+ $this->Attempt->fail('admin_login', $this->loginAttemptDuration);
+ $this->Session->setFlash('Unknown user or wrong password.');
+ return;
+ }
+ }
+ } else {
+ // $loginAttemptLimit reached
+ $this->Session->setFlash('Login limit exceeded, please try again later.');
+ }
+ }
+ }
10 Test/Case/AllAttemptTest.php
View
@@ -0,0 +1,10 @@
+<?php
+class AllAttemptTest extends CakeTestSuite {
+ public static function suite() {
+ $suite = new CakeTestSuite('All Attempt Plugin Tests');
+ $suite->addTestDirectory(APP.'Plugin'.DS.'Attempt'.DS.'Test'.DS.'Case'.DS.'Controller'.DS.'Component');
+ $suite->addTestDirectory(APP.'Plugin'.DS.'Attempt'.DS.'Test'.DS.'Case'.DS.'Model');
+ // $suite->addTestDirectoryRecursive(APP.'Plugin'.DS.'Attempt'.DS.'Test'.DS.'Case');
+ return $suite;
+ }
+}
94 Test/Case/Controller/Component/AttemptComponentTest.php
View
@@ -0,0 +1,94 @@
+<?php
+App::uses('Controller', 'Controller');
+App::uses('ComponentCollection', 'Controller');
+App::uses('AttemptComponent', 'Attempt.Controller/Component');
+
+/**
+ * TestProjectsController *
+ */
+class TestAttemptController extends Controller {
+ public $autoRender = false;
+}
+
+/**
+ * ProjectsController Test Case
+ *
+ */
+class AttemptComponentTestCase extends CakeTestCase {
+
+ public $AttemptComponent = null;
+ public $Controller = null;
+
+ public $fixtures = array('plugin.attempt.attempt');
+
+/**
+ * setUp method
+ *
+ * @return void
+ */
+ public function setUp() {
+ parent::setUp();
+
+ $Collection = new ComponentCollection();
+ $this->AttemptComponent = new AttemptComponent($Collection);
+
+ $request = new CakeRequest();
+ $this->Controller = new TestAttemptController($request);
+ $this->AttemptComponent->startup($this->Controller);
+ }
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+ public function tearDown() {
+ parent::tearDown();
+ unset($this->AttemptComponent);
+ unset($this->Controller);
+ }
+
+ public function testCount() {
+ $result = $this->AttemptComponent->count('admin_login');
+ $this->assertEquals(2, $result);
+
+ $result = $this->AttemptComponent->count('something');
+ $this->assertEquals(0, $result);
+ }
+
+ public function testLimit() {
+ $result = $this->AttemptComponent->limit('admin_login');
+ $this->assertTrue($result);
+
+ $result = $this->AttemptComponent->limit('admin_login', 2);
+ $this->assertFalse($result);
+
+ $result = $this->AttemptComponent->limit('something', 3);
+ $this->assertTrue($result);
+ }
+
+ public function testFail() {
+ $result = $this->AttemptComponent->fail('admin_login');
+ $this->assertEquals($result['Attempt']['ip'], '127.0.0.1');
+ $this->assertEquals($result['Attempt']['action'], 'admin_login');
+
+ $result = $this->AttemptComponent->fail('admin_login', '+20 minutes');
+ $this->assertEquals($result['Attempt']['ip'], '127.0.0.1');
+ $this->assertEquals($result['Attempt']['action'], 'admin_login');
+ }
+
+ public function testReset() {
+ $result = $this->AttemptComponent->reset('admin_login');
+ $this->assertTrue($result);
+
+ // deleteAll returns true no matter what.
+ $result = $this->AttemptComponent->reset('something');
+ $this->assertTrue($result);
+ }
+
+ public function testCleanup() {
+ $result = $this->AttemptComponent->cleanup();
+ $this->assertTrue($result);
+ }
+
+}
82 Test/Case/Model/AttemptTest.php
View
@@ -0,0 +1,82 @@
+<?php
+// App::uses('Attempt', 'Attempt.Model');
+
+/**
+ * Attempt Test Case
+ *
+ */
+class AttemptTestCase extends CakeTestCase {
+/**
+ * Fixtures
+ *
+ * @var array
+ */
+ public $fixtures = array('plugin.attempt.attempt');
+
+/**
+ * setUp method
+ *
+ * @return void
+ */
+ public function setUp() {
+ parent::setUp();
+ $this->Attempt = ClassRegistry::init('Attempt.Attempt');
+ }
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+ public function tearDown() {
+ unset($this->Attempt);
+
+ parent::tearDown();
+ }
+
+ public function testCount() {
+ $result = $this->Attempt->count('127.0.0.1', 'admin_login');
+ $this->assertEquals(2, $result);
+
+ $result = $this->Attempt->count('127.0.0.7', 'admin_something');
+ $this->assertEquals(0, $result);
+ }
+
+ public function testLimit() {
+ $result = $this->Attempt->limit('127.0.0.1', 'admin_login', 5);
+ $this->assertTrue($result);
+
+ $result = $this->Attempt->limit('127.0.0.1', 'admin_login', 1);
+ $this->assertFalse($result);
+
+ $result = $this->Attempt->limit('127.0.0.1', 'admin_login', 0);
+ $this->assertFalse($result);
+
+ $result = $this->Attempt->limit('127.0.0.7', 'admin_something', 5);
+ $this->assertTrue($result);
+
+ $result = $this->Attempt->limit('127.0.0.7', 'admin_something', 0);
+ $this->assertFalse($result);
+ }
+
+ public function testFail() {
+ $result = $this->Attempt->fail('127.0.0.1', 'admin_login', '1 hour');
+ $this->assertEquals($result['Attempt']['ip'], '127.0.0.1');
+ $this->assertEquals($result['Attempt']['action'], 'admin_login');
+ }
+
+ public function testReset() {
+ $result = $this->Attempt->reset('127.0.0.1', 'admin_login');
+ $this->assertTrue($result);
+
+ // deleteAll returns true no matter what.
+ $result = $this->Attempt->reset('127.0.0.9', 'admin_something');
+ $this->assertTrue($result);
+ }
+
+ public function testCleanup() {
+ $result = $this->Attempt->cleanup();
+ $this->assertTrue($result);
+ }
+
+}
51 Test/Fixture/AttemptFixture.php
View
@@ -0,0 +1,51 @@
+<?php
+/**
+ * AttemptFixture
+ *
+ */
+class AttemptFixture extends CakeTestFixture {
+
+/**
+ * Fields
+ *
+ * @var array
+ */
+ public $fields = array(
+ 'id' => array('type' => 'string', 'null' => false, 'length' => 36, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
+ 'ip' => array('type' => 'string', 'null' => true, 'default' => NULL, 'length' => 64, 'key' => 'index', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
+ 'action' => array('type' => 'string', 'null' => true, 'default' => NULL, 'length' => 32, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
+ 'created' => array('type' => 'datetime', 'null' => true, 'default' => NULL),
+ 'expires' => array('type' => 'datetime', 'null' => true, 'default' => NULL, 'key' => 'index'),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'ip' => array('column' => array('ip', 'action'), 'unique' => 0), 'expires' => array('column' => 'expires', 'unique' => 0)),
+ 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM')
+ );
+
+/**
+ * Records
+ *
+ * @var array
+ */
+ public $records = array(
+ array(
+ 'id' => '4f5a6edd-311c-43ab-8d38-464d3c87f3ee',
+ 'ip' => '127.0.0.1',
+ 'action' => 'admin_login',
+ 'created' => '2012-03-09 20:58:05',
+ 'expires' => '2012-03-09 20:58:05'
+ ),
+ array(
+ 'id' => '5f5a6edd-311c-43ab-8d38-464d3c87f3ee',
+ 'ip' => '127.0.0.1',
+ 'action' => 'admin_login',
+ 'created' => '2022-03-09 20:58:07',
+ 'expires' => '2022-03-09 20:58:07'
+ ),
+ array(
+ 'id' => '6f5a6edd-311c-43ab-8d38-464d3c87f3ee',
+ 'ip' => '127.0.0.1',
+ 'action' => 'admin_login',
+ 'created' => '2015-03-09 20:58:05',
+ 'expires' => '2015-03-09 20:58:05'
+ ),
+ );
+}

No commit comments for this range

Something went wrong with that request. Please try again.