Заметки за 2017 год

CSRF-атака на ваш API

Иван Гришаев в своей статье Руководство по кросс-доменным запросам (CORS) подробно рассказываем про механизм осуществления запроса на другой сервер. В частности, он описывает «простые» и «сложные» запросы.

Простым считается запрос методами:

  • HEAD
  • GET
  • POST

и заголовками:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type, но только со значениями:
    — application/x-www-form-urlencoded
    — multipart/form-data
    — text/plain

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

Вот такие простые запросы злоумышленники могут эксплуатировать для CSRF-атак.

fetch('https://jsfiddle.net/echo/html/', {
  method: 'POST',
  credentials: 'include',
  body: new URLSearchParams({
    html: 'My test response'
  })
});

Даже если ответ не был принят браузером, тем не менее сервер уже совершил какие-то операции. А так как вместе с запросом автоматически передаются cookie, то эти операции могу быть выполнены от лица авторизованного пользователя или администратора.

Как защититься?

  1. Принимаем только запросы с заголовком Content-Type: application/json.

    Это автоматически делаем запрос «сложным». Он будет выполнен в 2 этапа: сначала браузер проверит какие заголовки разрешены для CORS-запроса, а затем выполнит основной запрос.

  2. Правильно выставляем заголовки Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods и Access-Control-Max-Age.

  3. Авторизацию пользователя выполняем только по значению в заголовке, а не по cookie.

    Стоит отметить, что хранить авторизационный токен в cookie не опасно. Но использовать его на сервере нельзя так как все cookie для домена, на который выполняется запрос, как я уже упомянул, пересылаются автоматически.

    Заголовки же можно выставить только явно при формировании запроса. Но злоумышленник не будет иметь доступа к нужным данным в браузере и, соответственно, физически не сможет совершить запрос от лица авторизованного пользователя.

Оставте свой комментарий

Автоматическое развёртывание проекта при обновлении кода в репозитории

Я уже не представляю себе процесс разработки без системы контроля версий. Но помимо явного полезного действия, такие системы при определённых событиях могу вызывать различные скрипты (например, запрос по URL или отправка уведомлений на электронную почту). Этим можно воспользоваться для автоматического обновления кода проекта на сервере при наличии изменений в определённой ветке.

Коммит в репозиторий, сборка, копирование артефакта

В Интернете можно найти некоторое количество как платных, так и бесплатных сервисов и программ. У меня нет сложных задач, для которых требуются такие системы. Поэтому я пишу свои несложные скрипты для этого.

Для хостинга приватных Git-репозиториев я использую Bitbucket. Там есть настройка «Webhooks». Webhook — это URL, на который Bitbucket делает POST-запрос. Нам нужно написать скрипт, который бы вызывался бы на каждый запрос, загружал изменения из Git-запозитория и выполнял другие полезные действия.

Все команды и настройки были произведены на чистом сервере Ubuntu 17.04

Подготовка окружения

В файл /etc/apt/sources.list.d/nginx.list добавляем адрес репозитория с официальными пакетами nginx:


deb http://nginx.org/packages/ubuntu/ zesty nginx
deb-src http://nginx.org/packages/ubuntu/ zesty nginx

Теперь загрузим ключ, которым подписаны пакеты и установим nginx и fcgiwrap:


curl -L https://nginx.ru/keys/nginx_signing.key | apt-key add -
apt-get update
apt-get install nginx fcgiwrap

Добавим конфигурацию сервера, который будет принимать запросы от Git-репозитория. Создадим файл /etc/nginx/conf.d/deploy.conf со следующим содержимым:


server {
  listen 80;
  listen [::]:80;

  server_name deploy.my-cool-project.com;

  location /push-hook {
    gzip off;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    include /etc/nginx/fastcgi_params;
    fastcgi_param DOCUMENT_ROOT /var/www/deploy;
    fastcgi_param SCRIPT_FILENAME /var/www/deploy/deploy.sh;
  }
}

Каждый раз, когда nginx получить запрос по адресу http://deploy.my-cool-project.com/push-hook, на сервере будет выполнен скрипт /var/www/deploy/deploy.sh. Давайте создадим его.

mkdir -p /var/www/deploy
nano deploy.sh

Содержимое файла deploy.sh:

#!/bin/sh

echo "Content-type:text/plain\n"
echo "OK!"

Обязательно установим флаг «eXecute» у этого скрипта:

chmod 755 /var/www/deploy/deploy.sh

Теперь можно проверить как он работает:

curl http://ci.my-cool-project.com/push-hook

Клонирование репозитория

Перейдём к настройке git и первоначальному клонированию проекта на сервере.

apt-get install git

Генерируем ключ для доступа:

ssh-keygen -t rsa -f /var/www/deploy/access-key -N ""

Добавляем этот ключ в панели управления репозиторием. Ключ для доступа имеет права «только чтение» и специально создаётся для того, чтобы система CI/CD могла получать изменения.

Сохраняем публичный ключ удаленного сервера:

ssh-keyscan -t rsa bitbucket.org 2>&1 >> /etc/ssh/ssh_known_hosts

Команда ssh делает это автоматически при первом подключении к неизвестному хосту во время процедуры обмена ключами. Но сохраняет она этот публичный ключ в файл для пользователя, выполнившего команду, и только после положительного ответа на запрос. Чтобы команда git pull нормально выполнялась в фоновом процессе под пользователем www-data, публичный ключ нужно добавить вручную в общий файл.

Установим владельца папки /var/www/deploy и её содержимого:

chown -R www-data:www-data /var/www/deploy

Теперь можно клонировать репозиторий:


cd /var/www/deploy
GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
git clone git@bitbucket.org:mistakster/test-project.git ./repo
chown -R www-data:www-data ./repo

Так как мы используем специальный ключ для доступа к репозитории, то мы явно должны сообщить команде git о нём. Однако она не предоставляет соответствующей опции. Данная проблема решается через переменную окружения GIT_SSH_COMMAND . В ней можно указать любые настройки, которые нам нужны.

Подтягивание изменений

Когда мы запушим в репозиторий какие-то изменения, Git-сервер вызовет webhook и передаст информацию о них в виде JSON-объекта. В самом простом варианте, деплой можно запускать при любых изменениях. Но корректнее было бы отслеживать только изменения в нужной ветке, например, master.

Для парсинга JSON в bash-скрипте я буду использовать команду jq.


apt-get install jq

Обновим файл deploy.sh:


#!/bin/sh

echo "Content-type:text/plain\n"

if
  [ -n "$HTTP_X_HOOK_UUID" ] &&
  [ -n "$HTTP_X_REQUEST_UUID" ] &&
  [ "$HTTP_X_EVENT_KEY" = "repo:push" ] &&
  [ "$HTTP_CONTENT_TYPE" = "application/json" ] &&
  [ "$REQUEST_METHOD" = "POST" ]
then
  MASTER_COMMITS=`jq '[.push.changes[].new | select(.name == "master" and .type == "branch")] | length'`

  if [ "$MASTER_COMMITS" -gt 0 ]
  then
    cd ./repo

    GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
    git fetch --all 2>&1
    git reset --hard origin/master
    ./build.sh

    echo "Build complete"
  else
    echo "Build skipped"
  fi
else
  echo "Wrong request"
fi

Теперь мы ожидаем только POST-запрос с соответствующим заголовками и данными. Если в среди всех коммитов встретились изменения в ветке master , то запускается загрузка этих изменений из репозитория и вызов скрпита build.sh в корне проекта.

Фактические действия, необходимые для сборки, например, загрузка пакетов из npm-репозитория, запуск webpack, копирование артефактов и т.п., удобнее вынести именно в скрипт build.sh. Это вполне логично, так как для разных проектов требуются разные действия. К тому же он будет обновляться вместе с проектом и загружаться автоматически.

Теперь можно настроить webhook в Bitbucket.

Настройка webhook в Bitbucket

Заключение

Данное решение покрывает лишь минимально необходимые требования. Оно не является заменой популярных систем CI/CD. Тем не менее, оно стабильно, прекрасно подходит для экспериментальных проектов и заметно сокращает количество рутинных операций для небольшой команды разработчиков.

Оставте свой комментарий

util.promisify() для замены колбэков на промисы

Промисы на практике оказались удобнее колбэков из-за того, что не требуют постоянного увеличения вложенности при работе с цепочками асинхронных функций. Более того, на базе промисов строится работа async function.

Все асинхронные функции node.js и подавляющее большинство асинхронных функций внешних модулей на данный момент всё же используют колбэки, чтобы вернуть результат работы или сообщить об ошибке.

Технически, не сложно обернуть асинхронную функцию в промиз. Но, к сожалению, это не одна строчка кода.


const fs = require('fs');

function reader(path) {
    return new Promise((resolve, reject) => {
        fs.readdir(path, (err, list) => {
            if (err) {
                reject(err);
                return;
            }

            resolve(list);
        });
    });
}

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

В стандартной библиотеке node.js 8 util появилась функция promisify(), которая как раз решает эту задачу.


const fs = require('fs');
const util = require('util');

const reader = util.promisify(fs.readdir);

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

Функция promisify() придерживается соглашения:

  1. колбек в параметрах исходной функции всегда идет последним;
  2. первым параметром колбека приходит объект ошибки, если она есть.

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


function Timer(delay) {
    this._delay = delay;
}

Timer.prototype.start = function (callback) {
    setTimeout(callback, this._delay);
};

const timer = new Timer(1000);
timer.start(() => {
    console.log('Finish');
});

Из метода start() я хочу сделать новую функцию, возвращающую мне промиз.


const start = util.promisify(timer.start);
start().then(() => {
    console.log('Finish');
});

Но этот фрагмент кода перестаёт работать. Он начинает выдавать ошибку:

TypeError: Cannot read property '_delay' of undefined

Проблема кроется в том, что метод внутри себя использует ссылку на экземпляр класса Timer — в частности, он вытаскиваем величину задержки. А так как метод был вызван вне контекста экземпляра таймера, то this оказалось не определено.

Если вернуться к примеру с колбэком, то следующий фрагмент кода тоже не будет работать и выдаст такую же ошибку.


const start = timer.start;
start(() => {
    console.log('Finish');
});

Исправить ситуацию можно несколькими путями.

Связать функцию с экземпляром

const start = util.promisify(timer.start).bind(timer);

Теперь не важно как мы вызовем функцию start() . Она всегда будет иметь экземпляр таймера в качестве контекста исполнения.

Сохранить функцию в экземпляр класса


timer.start = util.promisify(timer.start);
timer.start().then(() => {
    console.log('Finish');
});

Для данного экземпляра мы подменили реализацию метода из прототипа на новую функцию. Контекст исполнения будет передаваться естественным образом.

Развивая этот вариант дальше, новый метод можно сохранить в прототипе класса.

Timer.prototype.start = util.promisify(Timer.prototype.start);

Такой вариант опасен тем, что логика работы изменится для абсолютно всех экземпляров данного класса. Возможно, это не страшно для кода, который вы сами пишете и обслуживаете. Но, вряд-ли будет приемлемо для сторонних модулей.

Оставте свой комментарий