Home > Article > Backend Development > Strengthening security measures for startups: A guide
This tutorial is part of the "Build Your Startup with PHP" series on Envato Tuts. In this series, I'll guide you through launching a startup from concept to reality, using my meeting planner app as a real-life example. Every step of the way, I'll release the Meeting Planner code as an open source example that you can learn from. I will also address startup-related business issues that arise.
In the last episode, I mainly introduced web server security and access control. In today's show, I'll discuss additional protections I've added to Meeting Planner. Since all the code was written in PHP's Yii2 framework, I was able to leverage the framework for a lot of fortifications. If you want to learn more about Yii2, check out our parallel series "Programming with Yii2."
You can try the Meeting Planner now by scheduling your first meeting. Please feel free to post feedback about your experience in the comments below. I'm also open to ideas for new features and topic suggestions for future tutorials.
Implementing different levels of security for Meeting Planner takes several hours. Now that the server is configured more robustly, I want to guide you through other security areas of your application code.
Obviously, it's important to keep authentication keys away from hackers, but it's also easy to publish them to GitHub. There have been reports of files being accidentally checked in using service passwords or API keys.
To prevent this from happening in Yii, I keep an external .ini file outside of the code tree. This is loaded at the top of /frontend/config/main.php and used for any necessary component configuration:
<?php $config = parse_ini_file('/var/secure/meetme.ini', true); $params = array_merge( require(__DIR__ . '/../../common/config/params.php'), require(__DIR__ . '/../../common/config/params-local.php'), require(__DIR__ . '/params.php'), require(__DIR__ . '/params-local.php') ); return [ 'id' => 'mp-frontend', 'name' => 'Meeting Planner', 'basePath' => dirname(__DIR__), 'bootstrap' => ['log'], 'controllerNamespace' => 'frontend\controllers', 'components' => [ 'authClientCollection' => [ 'class' => 'yii\authclient\Collection', 'clients' => [ 'facebook' => [ 'class' => 'yii\authclient\clients\Facebook', 'clientId' => $config['oauth_fb_id'], 'clientSecret' => $config['oauth_fb_secret'], ],
In the example above, you can see the Facebook API secret loaded from the initialization file.
The format of the initialization file is quite simple:
mysql_host="localhost" mysql_un="xxxxxxxxxxxxxxxxxxx" mysql_db="xxxxxxxxxxxxxxxxxxx" mysql_pwd="xxxxxxxxxxxxxxxxxxx" mailgun_user = "xxxxxxxxxxxxxxxxxxx@meetingplanner.io" mailgun_pwd = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" mailgun_api_key="key-9p-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" mailgun_api_url="https://api.mailgun.net/v2" mailgun_public_key="pubkey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" oauth_fb_id="1xxxxxxxxxxxxxxxxxxx3" oauth_fb_secret="bcxxxxxxxxxxxxxxxxxxxda"
Yii2 encourages you to place some of these settings in the /environments directory, especially if the settings differ between development and production.
Therefore, it is important that your .gitignore file excludes local versions of these files:
#local environment files /environments/prod/common/config/main-local.php /environments/prod/frontend/config/main-local.php /frontend/config/params-local.php /frontend/config/main-local.php
This is an example of one of my local parameter files, /frontend/config/params-local.php:
<?php return [ 'ga' => 'UA-xxxxxxxxxx-12', 'urlPrefix' => '', 'google_maps_key' => 'AIzzzzzz1111222222xxxxxxQ', ];
I could probably spend more time organizing these better.
For the Alpha version, I sent the update in batches. Also, in the early days of Meeting Planner, there were more bad emails than I expected. Mailgun makes it easy to identify bounces and failures:
$badEmails=[ '', 'test2@gmail.com', '1111@gmail.com', 'qwerty@gmail.com', 'amjadiqbalkhanniazi@gmail.com', 'admin@admin.com', 'rhizalpatra@fellow.lpkia.ac.id', 'tm@archi.com', 'test@test.com', 'web@yahoo.fr', 'a@a. a', 'ailaa@aa.com', 'be@yahoo.fr', 'vico@gmail.com', 'nobu@gmail.com', 'a@gmail.com', 'ct@gmail.com', 'sanjaydk@projectdemo. biz', 'trial@gmail.com', 'varlog255q@hotmail.com', 'baah@baah.com', 'minhvnn1@gmail.com', 'test@gmail.com', 'test@mediabite.co.uk', 'ddd@c. hu', 'ddd@ymail.com', 'a. chetan@saisoftex.com', 'user02@local.com', 'Imrky4@gmail.com', 'robomadybu@hotmail.com', 'mike@mike. mike', 'abcd@gmail.com', 'azazaz@azazaza.com', 'mama@mama.mn', 'qweqwe@qwe. qwe', 'testere@wp.pl', 'kaze@hotmail.com', 'test@usertest.fr', 'demodemo@demo.com', 'qqq@dd.gh', 'gnfbb@h. vo', 'admin@admin123.com', 'testsir@testsir.com', 'oi. hd@yeah1.vn', 'loi. hd@yeah1.vn', 'test@email.com', 'salom@salom.com', 'ar@yahoo.com', 'lex@gmail.com', 'Tester1234@gmail.com', 'mantaf@mail.com', 'aaa@aaa.com', 'oeui@gmail.com', 'risitesh. biswal14@yahoo.com', 'ttt@wp.pl', 'nnn@nnn.net', 'nnn2@nnn.net', 'ana@gmail.com', 'asdf@yahoo.com', 'noom@gmail.com', 'jomon@example.com', 'asdfasdf@yahoo.com', 'admin@yahoo.com', 'abinubli@mail.com', 'tes@tes.com', 'asdasdr@asd.com', 'something@some.com', 'ademin@example.com', 'd@dd.com', 'robo@gmail.com', 'toto@titi.com', 'fesfe@fseff. fes', 'master@wpthemeslist.com', 'teste@teste.com', 'barny182@hotmail.com', 'test@admin.com', 'billtian@test.com', 'Test@goggle.ca', 'jm@gmail.com', 'john-panin@qip.ru', 'loslos@loslos.com', 'ghfhf@jhgjgjk.com', 'lol@lol.com', 'tester1@gmail.com', 'g0952180828@gmail.com', 'testim@testim.com', 'mnml.name@gmail.com', 'endri. azizi. 92@gmail.com', '123123@gmail.com', 'myfriend@gmai.com', 'geraldo_1989@hotmail.com', 'rob. test. 999@gmail.com', 'j@c. com', 'Agung. andika@mhs.uinjkt.ac.id', 'W3test@ya.ru', 'user@ya.ru', 'ed@ed. fl', 'ed@ed.es', ];
Most of this probably came about when the meeting planner was idle when it first came out - during my brain tumor treatment and surgery.
Recently, I made it very easy to sign up for Meeting Planner by adding social login, but spamming sign-ups is still possible. I want to make it harder for people to sign up with a bad email.
Fortunately, Yii provides some functions to support this functionality.
Yii2 now provides built-in verification codes. Therefore, anyone registering using the old email and password method will have to enter a verification code. You can see the captcha
fields below:
<p>Or, fill out the following fields to register manually:</p> <div class="col-lg-5"> <?php $form = ActiveForm::begin(['id' => 'form-signup']); ?> <?= $form->field($model, 'username') ?> <?= $form->field($model, 'email', ['errorOptions' => ['class' => 'help-block' ,'encode' => false]])->textInput() ?> <?= $form->field($model, 'password')->passwordInput() ?> <?= $form->field($model, 'captcha')->widget(\yii\captcha\Captcha::classname(), [ // configure additional widget properties here ]) ?> <div class="form-group"> <?= Html::submitButton('Signup', ['class' => 'btn btn-primary', 'name' => 'signup-button']) ?> </div> <?php ActiveForm::end(); ?> </div>
Then, add the captcha compliance as a rule for the SignupForm
model:
<?php namespace frontend\models; use common\models\User; use yii\base\Model; use Yii; use yii\helpers\Html; use yii\validators\EmailValidator; /** * Signup form */ class SignupForm extends Model { public $username; public $email; public $password; public $captcha; /** * @inheritdoc */ public function rules() { return [ ['username', 'filter', 'filter' => 'trim'], ['username', 'required'], ['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'], ['username', 'string', 'min' => 2, 'max' => 255], ['email', 'filter', 'filter' => 'trim'], ['email', 'required'], ['email', 'email', 'checkDNS'=>true, 'enableIDN'=>true], ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken. '.Html::a('Looking for your password?', ['site/request-password-reset'])], ['password', 'required'], ['password', 'string', 'min' => 6], ['captcha', 'required'], ['captcha', 'captcha'], ]; }
If people don't enter the correct CAPTCHA response, they can't register. This makes it difficult for spammers to automate registrations.
I also want to minimize the use of fake email addresses for registrations. Yii's checkDNS
verification actually looks for a valid MX record based on the domain of the email address:
['email', 'email', 'checkDNS'=>true, 'enableIDN'=>true],
For example, if I mistakenly enter gmail.com as gmal.com, checkDNS
will return false
. gmal.com has no registered MX records. Likewise, spambotolympics9922.com does not.
Ultimately, security is an iterative process. There is always more to do.
Next, I'd like to add common limits on the number of actions people can perform to limit abuse and prevent the application from becoming unwieldy.
To prevent people from creating a lot of empty meetings, I created a findEmptyMeeting
which will find empty meetings and reuse it when someone tries to create a new one:
public function actionCreate() { // prevent creation of numerous empty meetings $meeting_id = Meeting::findEmptyMeeting(Yii::$app->user->getId()); //echo $meeting_id;exit; if ($meeting_id===false) { // otherwise, create a new meeting $model = new Meeting(); $model->owner_id= Yii::$app->user->getId(); $model->sequence_id = 0; $model->meeting_type = 0; $model->save(); $model->initializeMeetingSetting($model->id,$model->owner_id); $meeting_id = $model->id; } $this->redirect(['view', 'id' => $meeting_id]); }
In other words, if a user creates a new meeting 1,700 times, they will always see the first empty meeting they create.
I also created a withinLimit
method of a common structure to reuse around the application, this prevents too many operations from being performed in a short period of time. The following example checks whether the number of meetings created within the past hour and the last day does not exceed n:
public static function withinLimit($user_id,$minutes_ago = 180) { // how many meetings created by this user in past $minutes_ago $cnt = Meeting::find() ->where(['owner_id'=>$user_id]) ->andWhere('created_at>'.(time()-($minutes_ago*60))) ->count(); if ($cnt >= Meeting::NEAR_LIMIT ) { return false; } // check in last DAY_LIMIT $cnt = Meeting::find() ->where(['owner_id'=>$user_id]) ->andWhere('created_at>'.(time()-(24*3600))) ->count(); if ($cnt >= Meeting::DAY_LIMIT ) { return false; } return true; }
每当有人尝试创建会议时,我们都会检查 withinLimit
以查看他们是否可以。如果没有,我们会显示 flash
错误消息:
public function actionCreate() { if (!Meeting::withinLimit(Yii::$app->user->getId())) { Yii::$app->getSession()->setFlash('error', Yii::t('frontend','Sorry, there are limits on how quickly you can create meetings. Visit support if you need assistance.')); return $this->redirect(['index']); }
我还想限制操作的总数。例如,每个会议参与者只能为每次会议添加七个会议日期时间。在 MeetingTime.php 中,我设置了 MEETING_LIMIT
,以便稍后可以更改:
const MEETING_LIMIT = 7;
然后,MeetingTime::withinLimit()
检查以确保任何用户的建议次数不超过七次:
public static function withinLimit($meeting_id) { // how many meetingtimes added to this meeting $cnt = MeetingTime::find() ->where(['meeting_id'=>$meeting_id]) ->count(); // per user limit option: ->where(['suggested_by'=>$user_id]) if ($cnt >= MeetingTime::MEETING_LIMIT ) { return false; } return true; }
当他们去创建 MeetingTime
时,控制器创建方法会检查限制:
public function actionCreate($meeting_id) { if (!MeetingTime::withinLimit($meeting_id)) { Yii::$app->getSession()->setFlash('error', Yii::t('frontend','Sorry, you have reached the maximum number of date times per meeting. Contact support if you need additional help or want to offer feedback.')); return $this->redirect(['/meeting/view', 'id' => $meeting_id]); }
今天最后,我想确保对远程 cron 作业的访问安全。互联网上描述了一些有趣的方法。目前,我正在检查 $_SERVER['REMOTE_ADDR']
(请求 IP 地址)是否与托管 $_SERVER['SERVER_ADDR' 是同一服务器]
,本地IP地址。 $_SERVER['REMOTE_ADDR']
可以安全使用,换句话说,我已经了解到它无法被欺骗。
// only cron jobs and admins can run this controller's actions public function beforeAction($action) { // your custom code here, if you want the code to run before action filters, // which are triggered on the [[EVENT_BEFORE_ACTION]] event, e.g. PageCache or AccessControl if (!parent::beforeAction($action)) { return false; } // other custom code here if (( $_SERVER['REMOTE_ADDR'] == $_SERVER['SERVER_ADDR'] ) || (!\Yii::$app->user->isGuest && \common\models\User::findOne(Yii::$app->user->getId())->isAdmin())) { return true; } return false; // or false to not run the action }
对于我自己的测试,我还允许登录的管理员运行 cron 作业。
最终,我还可以为我的 cron 作业添加密码,并将它们移至命令行操作。
在过去的两集中,我已经完成了许多安全改进,但仍有更多工作要做。我的候选清单上包括对访问安全性的更深入审查,特别是通过 AJAX、IP 地址跟踪和阻止,以及仔细过滤所有用户输入。
再说一遍,你还在等什么?安排您的第一次会议,并在评论中分享您的反馈。我也非常感谢您对安全问题的评论。
与往常一样,您可以观看“使用 PHP 构建您的初创公司”系列中即将推出的教程,或关注我 @reifman。还有更多重要功能即将推出。
The above is the detailed content of Strengthening security measures for startups: A guide. For more information, please follow other related articles on the PHP Chinese website!