Номер карты в access.log: где ещё лежат данные, которых быть не должно
Однажды при разборе инцидента на клиентском сервере в access.log nginx нашёлся полный номер банковской карты — открытым текстом, в составе GET-запроса к форме оплаты. Никто не проектировал это специально: разработчик просто передал параметры платежа через query string, а веб-сервер, как и положено, записал полный URL в лог. Такие находки встречаются чаще, чем кажется, и почти никогда не замечаются, пока кто-то не начинает искать целенаправленно. Разберём, как это происходит, почему для платёжных данных это особенно плохо и что сделать, чтобы у вас в логах такого не было.
Содержание
- Как номер карты оказывается в access.log
- Почему это особенно рискованно именно для платёжных данных
- Кто и как долго на самом деле видит эти логи
- Не только карты: что ещё утекает в логи через URL
- Практика: никогда не передавать чувствительные данные через GET
- Аудит существующих логов: как искать уже утёкшие данные
Как номер карты оказывается в 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.
Если находка подтвердилась:
- Зафиксируйте масштаб — за какой период, сколько записей, сколько уникальных карт или пользователей затронуто. Здесь пригодится тот же подход, что при оценке масштаба любой утечки секрета: важно не гадать, а посчитать по логам, что именно и с какого момента было доступно.
- Удалите или обезличьте найденные записи в архивах и во всех системах, куда логи реплицировались — включая бэкапы. Простое
rmна одном сервере обычно не решает задачу целиком. - Закройте канал утечки — исправьте передачу данных на POST и добавьте фильтрацию в логировании, чтобы проблема не повторилась.
- Свяжитесь с платёжным процессором или банком-эквайером, если речь о номерах карт — решение о дальнейших шагах (уведомление держателей карт, перевыпуск) находится на их стороне и в их регламентах, это не то, что можно оценить своими силами постфактум.
Дальнейшие юридические и процедурные шаги по обязательным уведомлениям — отдельная и не универсальная тема, зависящая от юрисдикции и типа данных; тут стоит опираться на своего юриста или комплаенс-специалиста, а не на общую инженерную инструкцию.
Нужен сервер под эту задачу?
Разверните 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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →