Статьи

Создание вашего стартапа: повышение безопасности

Конечный продукт
Что вы будете создавать

Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.

В предыдущем выпуске я рассмотрел в первую очередь безопасность веб-сервера и контроль доступа . В сегодняшнем эпизоде ​​я расскажу о дополнительных мерах защиты, которые я добавил в Meeting Planner. Поскольку весь код написан на Yii2 Framework для PHP, я смог использовать эту среду для ряда этих укреплений. Если вы хотите узнать больше о Yii2, ознакомьтесь с нашей параллельной серией Программирование с Yii2 .

Вы можете попробовать Планировщик собраний прямо сейчас, запланировав свою первую встречу . Не стесняйтесь оставлять отзывы о вашем опыте в комментариях ниже. Я также открыт для новых идей и тематических предложений для будущих уроков.

Реализация различных уровней безопасности для Meeting Planner займет несколько эпизодов. Теперь, когда сервер настроен более надежно, я хочу рассказать вам о других областях безопасности для кода приложения.

Очевидно, что важно сохранять аутентификацию ключей вдали от хакеров, но также довольно легко публиковать их в GitHub. Рассказы о случайной регистрации файлов с паролем службы или ключом API.

Чтобы предотвратить это в Yii, я храню внешний файл .ini вне дерева кода. Это загружается в верхней части /frontend/config/main.php и используется для любой необходимой конфигурации компонента:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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’],
                  ],

В приведенном выше примере вы можете увидеть секреты API Facebook, загруженные из файла инициализации.

Формат файла инициализации довольно прост:

01
02
03
04
05
06
07
08
09
10
11
mysql_host=»localhost»
mysql_un=»xxxxxxxxxxxxxxxxxxx»
mysql_db=»xxxxxxxxxxxxxxxxxxx»
mysql_pwd=»xxxxxxxxxxxxxxxxxxx»
mailgun_user = «[email protected]»
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 рекомендует вам разместить некоторые из этих настроек в каталоге / сред, особенно если они различаются между разработкой и производством.

Таким образом, важно, чтобы ваш файл .gitignore исключал локальные версии этих файлов:

1
2
3
4
5
#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

Вот пример одного из моих локальных файлов параметров, /frontend/config/params-local.php:

1
2
3
4
5
6
<?php
return [
  ‘ga’ => ‘UA-xxxxxxxxxx-12’,
  ‘urlPrefix’ => »,
  ‘google_maps_key’ => ‘AIzzzzzz1111222222xxxxxxQ’,
];

Я мог бы потратить еще больше времени на их организацию.

Создание собственного запуска - предотвращение регистрации спама

Для альфа-релиза я разослал обновления волнами. И на ранних этапах Meeting Planner было больше плохих писем, чем я ожидал. Mailgun позволил легко идентифицировать отказов и отказов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
$badEmails=[ », ‘[email protected]’, ‘[email protected]’, ‘[email protected]’,
[email protected]’, ‘[email protected]’, ‘[email protected]’, ‘sanjaydk@projectdemo.
[email protected]’, ‘ddd@c.
[email protected]’, ‘[email protected]’, ‘mike@mike.
[email protected]’, ‘[email protected]’, ‘qweqwe@qwe.
[email protected]’, ‘oi.
[email protected]’, ‘risitesh.
[email protected]’, ‘endri.
[email protected]’, ‘rob.
[email protected]’, ‘[email protected]’, ‘ed@ed.

Большинство из них, вероятно, из-за разрыва во времени, когда Meeting Planner был новым и бездействующим — во время лечения опухоли головного мозга и операции .

Совсем недавно, добавив социальные учетные записи , я сделал регистрацию на Meeting Planner довольно простой, но регистрация спама все еще возможна. Я хотел, чтобы людям было труднее регистрироваться с плохими электронными письмами.

К счастью, Yii предлагает несколько функций, которые поддерживают это.

Yii2 теперь предлагает встроенную капчу. Таким образом, любой, кто регистрируется с помощью старого метода электронной почты и пароля, должен ввести капчу. Вы можете увидеть поле captcha ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<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>

Затем соответствие модели с капчей добавляется, как правило, для модели SignupForm :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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.
            [‘password’, ‘required’],
            [‘password’, ‘string’, ‘min’ => 6],
            [‘captcha’, ‘required’],
            [‘captcha’, ‘captcha’],
        ];
    }

Если люди не введут правильный ответ с картинки, они не смогут зарегистрироваться. Это затрудняет автоматическую регистрацию спаммеров.

Я также хотел свести к минимуму регистрацию с поддельным адресом электронной почты. Проверка checkDNS самом деле ищет действительную запись MX, основанную на домене адреса электронной почты:

1
[’email’, ’email’, ‘checkDNS’=>true, ‘enableIDN’=>true],

Так, например, если я неправильно набрал gmail.com как gmal.com, checkDNS вернет false . Для gmal.com нет зарегистрированной записи MX. Точно так же нет ни одного для spambotolympics9922.com.

В конечном счете, безопасность — это итеративный процесс. Всегда есть чем заняться.

Затем я хотел добавить общие ограничения на количество действий, которые могут выполнять люди, чтобы ограничить злоупотребления и не дать приложению стать громоздким.

Чтобы люди не findEmptyMeeting создавать множество пустых собраний, я создал findEmptyMeeting   который ищет пустую встречу и использует ее, когда кто-то пытается создать новую:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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]);
   }

Другими словами, если пользователь отправляется на создание новой встречи 1700 раз, ему всегда будет представлена ​​первая пустая встреча, которую он создал.

Я также создал обычно структурированный метод withinLimit для повторного использования вокруг приложения, который может предотвратить слишком много действий за слишком короткое время. В приведенном ниже примере проверяется, что за последний час и последний день было создано не более n заседаний:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 чтобы узнать, могут ли они. Если нет, мы показываем сообщение об ошибке:

1
2
3
4
5
6
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 , чтобы его можно было изменить позже:

1
const MEETING_LIMIT = 7;

Затем MeetingTime::withinLimit() проверяет, чтобы любой пользователь предлагал не более семи раз:

01
02
03
04
05
06
07
08
09
10
11
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 , метод создания контроллера проверяет пределы:

1
2
3
4
5
6
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'] безопасно использовать для безопасности — другими словами, я читал, что его нельзя подделать.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// 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, eg 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;
   }

Для собственного тестирования я также разрешаю вошедшему в систему администратору запускать задания cron.

В конце концов, я также могу добавить пароль в мои задания cron и переместить их в операции командной строки.

За последние два эпизода я выполнил множество улучшений безопасности, но еще многое предстоит сделать. В моем списке есть более глубокий обзор безопасности доступа, особенно через AJAX, отслеживание и блокировка IP-адресов, а также тщательная фильтрация всего пользовательского ввода.

Опять чего вы ждете? Запланируйте свою первую встречу и поделитесь своим мнением в комментариях. Буду также признателен за ваши комментарии по вопросам безопасности.

Как всегда, вы можете посмотреть предстоящие уроки в серии « Построение стартапа с помощью PHP» или подписаться на меня @reifman . Есть еще несколько важных функций.