MAATRIX / Блог / Номер карты в access.log: где ещё лежат данные, которых быть не должно

Номер карты в access.log: где ещё лежат данные, которых быть не должно

MAATRIX

Однажды при разборе инцидента на клиентском сервере в access.log nginx нашёлся полный номер банковской карты — открытым текстом, в составе GET-запроса к форме оплаты. Никто не проектировал это специально: разработчик просто передал параметры платежа через query string, а веб-сервер, как и положено, записал полный URL в лог. Такие находки встречаются чаще, чем кажется, и почти никогда не замечаются, пока кто-то не начинает искать целенаправленно. Разберём, как это происходит, почему для платёжных данных это особенно плохо и что сделать, чтобы у вас в логах такого не было.

Как номер карты оказывается в access.log

Access log — это стандартный журнал веб-сервера (nginx, Apache, любой reverse proxy), куда построчно пишется каждый HTTP-запрос: метод, полный путь с параметрами, статус ответа, User-Agent, referer. Это базовая диагностика: без него невозможно расследовать 502-е, искать медленные запросы или ловить брутфорс. Проблема не в самом логе, а в том, что в него попадает.

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

GET /checkout/confirm?card=4111111111111111&exp=0928&amount=15000 HTTP/1.1

Стандартная конфигурация nginx это запишет один в один:

192.0.2.10 - - [28/Aug/2026:14:12:03 +0300] "GET /checkout/confirm?card=4111111111111111&exp=0928&amount=15000 HTTP/1.1" 200 512 "-" "Mozilla/5.0"

Дальше этот файл живёт своей жизнью: ротируется, архивируется, копируется в систему централизованного сбора логов (например, через связку с Graylog или Loki), попадает в бэкапы, иногда пересылается во внешний мониторинг. На каждом из этих шагов данные расходятся по системам, которые никогда не проектировались как хранилище платёжной информации.

Второй частый канал — не сам запрос, а то, что он тянет за собой. Если платёжные данные оказались в URL один раз, они автоматически попадут ещё и в:

  • referer следующего запроса — браузер честно передаёт полный адрес предыдущей страницы, включая query string;
  • логи CDN и прокси — весь путь запроса, если данные шли через промежуточные узлы;
  • аналитические системы, если на странице подключён любой скрипт аналитики, который логирует document.location или собирает клики;
  • логи приложения — если фреймворк логирует входящие запросы целиком «для отладки» и это не отключили в проде.

Иными словами, одна ошибка на входе (данные в GET) превращается в утечку сразу по нескольким независимым системам, и вычистить её из всех мест бывает сложнее, чем кажется на старте.

Почему это особенно рискованно именно для платёжных данных

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

Во-первых, индустрия платёжных карт (тот самый комплекс требований, который в обиходе называют PCI DSS) в целом исходит из простого принципа: полный номер карты (PAN) должен обрабатываться и храниться только в системах, которые для этого специально аттестованы и защищены — с шифрованием, ограниченным доступом, отдельным контуром безопасности. Я намеренно не берусь цитировать точные формулировки и номера требований — это тема для юриста или специалиста по комплаенсу, а не для инженерной статьи. Но сам принцип общеизвестен: обычный access.log на веб-сервере к таким системам не относится ни разу.

Во-вторых, access log — это по определению файл с широким кругом читателей. Его смотрят при любой диагностике: разработчик, ищущий баг, дежурный админ, скрипт мониторинга, внешний подрядчик, которому дали временный доступ для расследования инцидента. Доступ к боевой базе данных обычно кто-то контролирует и логирует отдельно; доступ «посмотреть последние строчки лога» — почти никогда. Это ровно тот тип утечки, который бьёт токены и персональные данные, случайно осевшие в логах: формально данные никто не «воровал», они просто лежали там, где их не должно было быть, и их увидели те, кому видеть не полагалось.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Кто и как долго на самом деле видит эти логи

Стоит честно оценить, сколько систем реально касаются access.log за его жизненный цикл, прежде чем считать утечку локальной проблемой одного файла:

ТочкаЧто происходит с данными
Веб-сервер (nginx/Apache)Запись в текущий лог-файл, обычно без шифрования на диске
Ротация логовАрхивные .gz-файлы хранятся неделями или месяцами по регламенту
Централизованный сборКопия уходит в Graylog / Grafana Loki / ELK и остаётся там отдельным индексом
Бэкапы сервераПолный снапшот диска или архив /var/log попадает в бэкап-хранилище
Внешний мониторинг/SIEMЕсли подключена интеграция, данные реплицируются в третью систему
Support-доступПодрядчик или новый сотрудник, получивший временный SSH-доступ, видит файл целиком

Каждая строка в этой таблице — отдельная точка, где номер карты может быть скопирован, проиндексирован или просто увиден человеком, у которого нет и не должно быть допуска к платёжным данным. И что важнее — ни одна из этих систем обычно не проектировалась с расчётом на то, что в ней может оказаться PAN: у логов нет шифрования на уровне поля, нет журнала «кто именно это прочитал», нет автоматического обезличивания.

Не только карты: что ещё утекает в логи через URL

Номер карты — самый тяжёлый по последствиям пример, но механизм универсален для любых чувствительных данных, переданных через GET. В логах регулярно находятся:

  • токены сессий и API-ключи в параметрах вида ?token=... или ?api_key=... — отдельная и частая история, разобранная в статье про то, как найти утёкший API-ключ по логам;
  • пароли, если форма логина по ошибке отправляется методом GET вместо POST;
  • персональные данные — email, телефон, паспортные данные — в параметрах восстановления пароля или предзаполненных форм;
  • внутренние идентификаторы и токены сброса пароля, которые рассылаются по email со ссылкой вида /reset?token=... — сама ссылка кликабельна, а значит попадёт и в access.log, и в почтовые логи, и в историю браузера.

Общий вывод отсюда простой: если данные оказались в URL, они гарантированно окажутся в нескольких логах одновременно, а не в одном. Это не теоретическая уязвимость — это то, что видно в любом access.log, если знать, что искать. Кстати, тот же самый файл в норме прекрасно показывает и попытки SQL-инъекций — что видно в access.log при атаке — то есть это в принципе богатый источник и для защиты, и для случайных утечек одновременно.

Практика: никогда не передавать чувствительные данные через GET

Главное правило закрывает большую часть проблемы ещё на этапе проектирования: чувствительные данные никогда не должны попадать в URL — ни в query string, ни в path. Это касается номеров карт, паролей, токенов, персональных данных. Причина проста и не зависит от конкретного сервера: URL по умолчанию логируется практически всеми компонентами инфраструктуры — веб-сервером, прокси, CDN, средствами мониторинга, — и попадает в history браузера и в referer.

Что делать вместо этого:

Используйте POST с телом запроса. Тело POST-запроса в стандартной конфигурации веб-сервера в access log не попадает — логируются только метод, путь и заголовки, но не тело. Это не «шифрование» и не панацея (тело всё ещё можно залогировать на уровне приложения, если включить debug-режим), но это убирает самый массовый и неконтролируемый канал утечки.

# Плохо — данные окажутся в query string и в access.log
location /checkout/confirm {
    # GET /checkout/confirm?card=...&exp=...
}

# Хорошо — данные идут в теле POST, вне access.log по умолчанию
location /checkout/confirm {
    limit_except POST { deny all; }
}

Для платёжных операций используйте готовый механизм платёжного шлюза, а не самописную передачу данных карты через ваш сервер. Большинство современных провайдеров эквайринга дают виджет или redirect-flow, где номер карты вводится прямо в форме на стороне платёжного процессора (iframe, hosted page) и никогда физически не проходит через ваше приложение и ваш веб-сервер. Это снимает вопрос «как не залогировать карту» полностью — её у вас просто никогда нет.

Если данные всё же обязаны пройти через ваш бэкенд (например, промежуточная обработка перед отправкой в процессинг), явно исключайте чувствительные поля на уровне логирования — и на уровне веб-сервера, и на уровне приложения. Пример для nginx: маскирование конкретных query-параметров через кастомный log_format с map:

map $arg_card $card_masked {
    default "[REDACTED]";
    ""      "";
}

log_format masked '$remote_addr - [$time_local] "$request_method $uri" '
                   '$status card=$card_masked';

access_log /var/log/nginx/access.log masked;

Это грубый, но рабочий рубеж защиты на уровне инфраструктуры — даже если разработчик где-то ошибётся и передаст карту в GET, конкретно это поле в лог не попадёт. На уровне приложения то же самое стоит делать с любым логированием запросов и ошибок: явный allowlist или denylist полей перед записью в лог, а не «пишем всё как есть, потом разберёмся».

Аудит существующих логов: как искать уже утёкшие данные

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

# Грубый поиск последовательностей, похожих на номер карты (13-19 цифр,
# возможно с разделителями), в архивных access-логах
zgrep -E '[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{1,7}' \
    /var/log/nginx/access.log*.gz | less

Это даст много ложных срабатываний (телефоны, ID заказов, любые длинные числа) — регулярка нужна как первый фильтр, а не как окончательный вердикт. Дальше стоит вручную просмотреть совпадения и отдельно проверить самые чувствительные эндпоинты — всё, что связано с оплатой, восстановлением пароля, вводом персональных данных. То же самое имеет смысл сделать по логам приложения, а не только по access.log веб-сервера — если фреймворк логирует полные запросы при исключениях, чувствительные поля могли попасть туда даже при отправке через POST.

Если находка подтвердилась:

  1. Зафиксируйте масштаб — за какой период, сколько записей, сколько уникальных карт или пользователей затронуто. Здесь пригодится тот же подход, что при оценке масштаба любой утечки секрета: важно не гадать, а посчитать по логам, что именно и с какого момента было доступно.
  2. Удалите или обезличьте найденные записи в архивах и во всех системах, куда логи реплицировались — включая бэкапы. Простое rm на одном сервере обычно не решает задачу целиком.
  3. Закройте канал утечки — исправьте передачу данных на POST и добавьте фильтрацию в логировании, чтобы проблема не повторилась.
  4. Свяжитесь с платёжным процессором или банком-эквайером, если речь о номерах карт — решение о дальнейших шагах (уведомление держателей карт, перевыпуск) находится на их стороне и в их регламентах, это не то, что можно оценить своими силами постфактум.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

POST-запросы точно не логируются веб-сервером?

По умолчанию nginx и Apache пишут в access log метод, путь и заголовки, но не тело запроса — так что да, тело POST туда не попадает без специальной настройки. Но при этом debug-логирование на уровне приложения (например, полное логирование входящих запросов при ошибке) вполне может залогировать и тело — это нужно проверять отдельно.

Если я использую HTTPS, разве данные в URL не защищены?

HTTPS шифрует канал передачи между клиентом и сервером, но не то, что сервер делает с данными после получения. Полный URL приходит на сервер расшифрованным и точно так же попадает в access.log — HTTPS от этой проблемы не спасает вообще никак.

Referer точно передаёт query string на другой домен?

Поведение зависит от заголовка Referrer-Policy и настроек браузера, но по умолчанию во многих конфигурациях полный URL, включая query string, действительно уходит в referer следующего запроса, в том числе на сторонние домены (аналитика, внешние скрипты). Явная настройка Referrer-Policy: no-referrer или same-origin снижает этот риск.

Достаточно ли просто настроить ротацию и короткий срок хранения логов, чтобы снять проблему?

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

Как быть с уже существующими интеграциями, которые присылают данные через GET (редиректы от партнёров)?

Там, где формат запроса не под вашим контролем, единственный надёжный вариант — принимать такой запрос на отдельном эндпоинте с немедленным исключением его из логирования (или с маскированием конкретных параметров), а не пытаться повлиять на партнёра. Идеально — просить у партнёра переход на POST или webhook-схему.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →