RequestToken will persist the token for the life of the client session, and generate
per-request keys that will match against that token.
Using these token/key pairs in forms and other non-idempotent requests will help you secure
your application against cross-site request forgeries, or CSRF attacks.
### Example
views/comments/add.html.php:
...
form->create($object); ?>
security->requestToken(); ?>
Other fields...
form->end(); ?>
controllers/CommentsController.php:
public function add() {
if ($this->request->data && !RequestToken::check($this->request)) {
Key didn't match the CSRF token. Regenerate the session token and
prompt the user to retry the form submission.
RequestToken::get(array('regenerate' => true));
return;
}
Handle a normal request...
}
protected function _init() { parent::_init(); # Check CSRF forgery signature if ($this->request->data and !RequestToken::check($this->request)) { throw new \Exception('Invalid request token.'); } if (isset($this->request->data['security']['token'])) { unset($this->request->data['security']); } # Load active user $current_identity = Auth::check('any'); if (is_object($current_identity)) { $u = $current_identity->getUser(); $this->CURRENT_USER = $u; } $this->set(array('CURRENT_USER' => $this->CURRENT_USER)); }
/** * Tests extracting a key from a `Request` object and matching it against a token. */ public function testTokenFromRequestObject() { $request = new Request(array('data' => array('security' => array('token' => RequestToken::key())))); $this->assertTrue(RequestToken::check($request)); }
/** * Remove directory or file * * html:method POST */ public function remove() { if ($this->request->is('ajax') && $this->request->data) { if (!RequestToken::check($this->request->data['token'])) { return $this->render(array('json' => array('regenerate' => true))); } $success = true; $errors = array(); foreach ($this->request->data['selected'] as $path) { $remove = Location::remove($path); if (!$remove) { $errors[] = array('error' => "<strong>{$path}</strong> not removed"); } if (!$copy && $success) { $success = false; } } return $this->render(array('json' => compact('success', 'errors'))); } return $this->redirect($this->_link); }
/** * Allows a user to update their own profile. * */ public function update() { if (!$this->request->user) { FlashMessage::write('You must be logged in to do that.', 'default'); return $this->redirect('/'); } // Special rules for user creation (includes unique e-mail) $rules = array('email' => array(array('notEmpty', 'message' => 'E-mail cannot be empty.'), array('email', 'message' => 'E-mail is not valid.'), array('uniqueEmail', 'message' => 'Sorry, this e-mail address is already registered.'))); // Get the document from the db to edit $conditions = array('_id' => $this->request->user['_id']); $document = User::find('first', array('conditions' => $conditions)); $existingProfilePic = !empty($document->profilePicture) ? $document->profilePicture : false; // Redirect if invalid user...This should not be possible. if (empty($document)) { FlashMessage::write('You must be logged in to do that.', 'default'); return $this->redirect('/'); } // If data was passed, set some more data and save if ($this->request->data) { // CSRF if (!RequestToken::check($this->request)) { RequestToken::get(array('regenerate' => true)); } else { $now = new MongoDate(); $this->request->data['modified'] = $now; // Add validation rules for the password IF the password and password_confirm field were passed if (isset($this->request->data['password']) && isset($this->request->data['passwordConfirm']) && (!empty($this->request->data['password']) && !empty($this->request->data['passwordConfirm']))) { $rules['password'] = array(array('notEmpty', 'message' => 'Password cannot be empty.'), array('notEmptyHash', 'message' => 'Password cannot be empty.'), array('moreThanFive', 'message' => 'Password must be at least 6 characters long.')); // ...and of course hash the password $this->request->data['password'] = Password::hash($this->request->data['password']); } else { // Otherwise, set the password to the current password. $this->request->data['password'] = $document->password; } // Ensure the unique e-mail validation rule doesn't get in the way when editing users // So if the user being edited has the same e-mail address as the POST data... // Change the e-mail validation rules if (isset($this->request->data['email']) && $this->request->data['email'] == $document->email) { $rules['email'] = array(array('notEmpty', 'message' => 'E-mail cannot be empty.'), array('email', 'message' => 'E-mail is not valid.')); } // Set the pretty URL that gets used by a lot of front-end actions. // Pass the document _id so that it doesn't change the pretty URL on an update. $this->request->data['url'] = $this->_generateUrl($document->_id); // Do not let roles or user active status to be adjusted via this method. if (isset($this->request->data['role'])) { unset($this->request->data['role']); } if (isset($this->request->data['active'])) { unset($this->request->data['active']); } // Profile Picture if (isset($this->request->data['profilePicture']['error']) && $this->request->data['profilePicture']['error'] == UPLOAD_ERR_OK) { $rules['profilePicture'] = array(array('notTooLarge', 'message' => 'Profile picture cannot be larger than 250px in either dimension.'), array('invalidFileType', 'message' => 'Profile picture must be a jpg, png, or gif image.')); list($width, $height) = getimagesize($this->request->data['profilePicture']['tmp_name']); // Check file dimensions first. // TODO: Maybe make this configurable. if ($width > 250 || $height > 250) { $this->request->data['profilePicture'] = 'TOO_LARGE.jpg'; } else { // Save file to gridFS $ext = substr(strrchr($this->request->data['profilePicture']['name'], '.'), 1); switch (strtolower($ext)) { case 'jpg': case 'jpeg': case 'png': case 'gif': case 'png': $gridFile = Asset::create(array('file' => $this->request->data['profilePicture']['tmp_name'], 'filename' => (string) uniqid(php_uname('n') . '.') . '.' . $ext, 'fileExt' => $ext)); $gridFile->save(); break; default: $this->request->data['profilePicture'] = 'INVALID_FILE_TYPE.jpg'; //exit(); break; } // If file saved, set the field to associate it (and remove the old one - gotta keep it clean). if (isset($gridFile) && $gridFile->_id) { if ($existingProfilePic && substr($existingProfilePic, 0, 4) != 'http') { $existingProfilePicId = substr($existingProfilePic, 0, -strlen(strrchr($existingProfilePic, '.'))); // Once last check...This REALLY can't be empty, otherwise it would remove ALL assets! if (!empty($existingProfilePicId)) { Asset::remove(array('_id' => $existingProfilePicId)); } } // TODO: Maybe allow saving to disk or S3 or CloudFiles or something. Maybe. $this->request->data['profilePicture'] = (string) $gridFile->_id . '.' . $ext; } else { if ($this->request->data['profilePicture'] != 'INVALID_FILE_TYPE.jpg') { $this->request->data['profilePicture'] = null; } } } } else { $this->request->data['profilePicture'] = null; } // Save if ($document->save($this->request->data, array('validate' => $rules))) { FlashMessage::write('You have successfully updated your user settings.', 'default'); $this->redirect(array('library' => 'li3b_users', 'controller' => 'users', 'action' => 'update')); } else { $this->request->data['password'] = ''; FlashMessage::write('There was an error trying to update your user settings, please try again.', 'default'); } } } $this->set(compact('document')); }
/** * 验证token * @param string $token * @return boolean */ public static function check($token) { $check = RequestToken::check($token); self::generate(array('regenerate' => true)); return $check; }