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

Начните с того места, куда попадает заявка

Браузер отправляет запрос на публичную конечную точку, которая записывает строку в контролируемое хранилище приёма и возвращает не больше чем подтверждение. Это хранилище — точка передачи, и оно должно быть единственным, куда может писать публичный уровень. Храните там необработанные данные без изменений, вместе со временем поступления и стабильным идентификатором записи.

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

curl --silent --fail \
  --cert /etc/importer/client.crt --key /etc/importer/client.key \
  "https://intake.corp.internal/api/pending?limit=20" \
  -o /var/lib/importer/pending.json

Дайте импортёру только те права, которые ему нужны

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

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

Краткоживущая функция не освобождается от той же проверки. Пока она работает, её сетевое положение и учётные данные всё ещё дают права, а короткий срок жизни лишь сужает окно. Срок жизни не заменяет решение о том, какие операции может выполнять субъект.

Проверяйте до того, как что-то изменится внутри

Поля публичной формы — это контролируемый злоумышленником текст, пока не доказано обратное, и рекомендации OWASP по проверке ввода относят серверные потоки к недоверенным источникам именно поэтому. Зашифрованная передача означает, что байты пришли целыми, а не что они описывают допустимый запрос. Разбирайте и проверяйте до любого привилегированного шага, чтобы плохая строка никогда не дошла до кода, способного действовать.

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

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

Сделайте точку повтора на одной операции

Если запись submission-1042 вытягивается дважды, потому что первое подтверждение потерялось, обработчику нужен устойчивый способ распознать исходную запись и её зафиксированный результат. Расписание, которое запускает задачу один раз, не доказывает, что каждая строка применена один раз. Повторная доставка нормальна, поэтому проект должен её поглощать.

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

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

for f in fixtures/valid-submission.json fixtures/bad-date.json; do
  curl --silent --fail --cert /etc/importer/client.crt \
    -X POST --data @"$f" https://intake.corp.internal/api/records
done
curl --silent --fail --cert /etc/importer/client.crt \
  -X POST --data @fixtures/valid-submission.json https://intake.corp.internal/api/records

Первые две строки должны закончиться в разных внутренних состояниях, а третья должна вернуть идентификатор, уже зафиксированный для этой исходной записи, вместо создания второго. Всё остальное означает, что импорт пока не повторяем.

Похожие статьи