Как только в системе появляются банковские карты, появляется и аббревиатура PCI DSS — и обычно в тревожном контексте: «мы попадаем под PCI?», «аудит через квартал». Разберём стандарт глазами разработчика: когда он вас касается, что реально требует и почему главное архитектурное решение — сделать так, чтобы касался как можно меньше.
Насколько глубоко стандарт вас касается, решает одно: проходит ли номер карты через ваши системы.
Редирект или iframe провайдера: номер не заходит в ваши системы — 0 из 5, опросник SAQ A. Своя форма: страница и её скрипты уже в CDE — 2 из 5, SAQ A-EP. Своя форма со своим хранением: в CDE и сервис, и БД, и логи — 5 из 5, SAQ D и аудитор на площадке. Способ приёма выбирают до реализации: он и определяет объём проверки.
Что это и когда касается вас
PCI DSS (Payment Card Industry Data Security Standard) — стандарт безопасности, созданный платёжными системами (Visa, Mastercard и другими). Он обязателен для всех, кто хранит, обрабатывает или передаёт данные платёжных карт. Это не закон государства, а договорное требование: нарушили — штрафы от банка-эквайера и вплоть до запрета принимать карты.
Версию стоит называть вслух: действующая редакция — PCI DSS 4.0.1, она вышла в июне 2024 года и заменила 4.0. Предыдущее поколение стандарта (3.2.1) больше не действует. Разница не косметическая: часть требований 4.x касается именно разработчика, и о них — ниже.
Ключевые термины:
- PAN (Primary Account Number) — номер карты; главный защищаемый объект;
- SAD (Sensitive Authentication Data) — CVV, содержимое магнитной полосы, PIN: после авторизации хранить нельзя вообще, даже зашифрованно. До авторизации эти данные можно держать в памяти — иначе платёж просто не провести, — но дальше памяти они уходить не должны. На языке кода это значит: ни в базу, ни в кэш, ни в очередь, ни в трассировку и, самое частое нарушение, ни в лог. Одна строка «залогируем тело запроса, чтобы разобраться» — и правило нарушено;
- CDE (Cardholder Data Environment) — все системы, которые хранят, обрабатывают или передают карточные данные, плюс всё, что с ними напрямую связано сетью.
Главная стратегия: сузить CDE
Требования PCI DSS распространяются на весь CDE — и на людей, и на процессы вокруг него. Поэтому центральный архитектурный ход — чтобы карточные данные вообще не попадали в ваши системы:
- Платёжный провайдер + токенизация. Форма оплаты рисуется провайдером (redirect или его iframe на вашей странице); PAN уходит напрямую провайдеру, вам возвращается токен — суррогат без ценности для вора. Ваши списания идут по токену. Большинство сервисов электронной коммерции живут именно так — их область аудита минимальна (самоопросник SAQ A вместо полного аудита).
- Сегментация. Если карты всё же обрабатываются у вас (платёжный шлюз, процессинг), CDE выделяется в изолированный сетевой сегмент: отдельные сервисы, свой контур доступа, минимум связей наружу. Каждый сервис, добавленный в CDE, — плюс к площади аудита; проектируйте так, чтобы «обычные» сервисы (каталог, заказы, профили) в CDE не входили.
Правило на языке кода: сервис заказов должен знать «оплачено токеном tok_abc», и никогда — номер карты.
Способы приёма и их цена
Как именно карта попадает к провайдеру — главное решение: оно определяет, видит ли ваш код номер вообще, и, как следствие, глубину проверки (какой опросник SAQ вы заполняете).
| Способ приёма | Кто касается номера карты | Скоуп PCI | Опросник | Усилия |
|---|---|---|---|---|
| Редирект на страницу провайдера | только провайдер | минимальный | SAQ A | низкие |
| iframe / hosted fields провайдера | только провайдер (поля — на его домене) | минимальный | SAQ A | низкие |
| Своя форма, PAN уходит из браузера прямо провайдеру (его скрипт или SDK) | провайдер, но страницу и скрипты на ней отдаёте вы | средний | SAQ A-EP | средние |
| Своя форма → ваш бэкенд → API провайдера | ваш сервер принимает PAN, даже если не хранит | максимальный | SAQ D | высокие |
| Своя форма + собственное хранение PAN | вы храните и обрабатываете PAN | максимальный | SAQ D + аудит QSA | очень высокие |
Третья и четвёртая строки выглядят похоже, а разница между ними — самая дорогая во всей таблице. В третьей номер карты из браузера уходит сразу провайдеру: ваш сервер его не видит вообще, вы отвечаете только за саму страницу и за скрипты на ней. В четвёртой форма отправляет номер на ваш бэкенд, и уже он зовёт API провайдера. Слово «не храним» тут ничего не меняет: PAN прошёл через вашу память, ваши логи, ваш трейсинг — сервер попал в CDE, и вместо анкеты на пару страниц вы заполняете SAQ D. Если в обсуждении звучит «своя форма, но мы же ничего не храним» — первым делом уточните, куда именно уходит номер.
Для третьего варианта (SAQ A-EP) есть два требования, которые касаются разработчика страницы напрямую и до которых обычно не доходят руки: вести учёт всех скриптов платёжной страницы с обоснованием, зачем каждый нужен, и иметь механизм, который заметит их подмену. В PCI DSS 4.x это пункты 6.4.3 и 11.6.1; переходный период кончился, они обязательны с 31 марта 2025 года. Смысл прямой: если на вашу страницу оплаты подсадят лишний скрипт, он прочитает поля формы до того, как номер уйдёт провайдеру.
Разница между первыми и последними строками — это разница между заполнить анкету на пару страниц и приглашать аудитора. Поэтому по умолчанию берут редирект или hosted fields; своя форма и тем более своё хранение — только когда без этого действительно нельзя.
3-D Secure: кто отвечает за спорную операцию
Первое, что спрашивают у бэкендера про платежи, — не про шифрование, а про спорные операции: карту украли, ею оплатили заказ, настоящий владелец потребовал деньги назад. Этот возврат называется chargeback, и вопрос только в том, из чьего кармана он пойдёт.
3-D Secure — шаг подтверждения на стороне банка, выпустившего карту: покупателя отправляют на страницу его банка, где он подтверждает платёж. Современная версия (EMV 3DS, её же называют 3DS2) в большинстве случаев проходит незаметно: провайдер передаёт банку данные об устройстве и поведении, банк оценивает риск и пропускает платёж без вопросов, а подтверждение просит только когда риск высокий.
Главное следствие деловое: при успешном подтверждении ответственность за мошенническую операцию переходит к банку-эмитенту, без подтверждения она остаётся на магазине. Именно поэтому 3DS включают не «для безопасности вообще», а чтобы не платить за чужие кражи; эквайер обычно этого и требует.
Для кода это означает, что платёж перестаёт быть одним вызовом. Провайдер в ответ может сказать не «оплачено» и не «отказ», а «нужно подтверждение» и дать адрес, куда отправить покупателя. Отсюда три требования к вашей модели:
- Статус «ждёт подтверждения». Заказ в этом состоянии не оплачен и не отклонён, товар по нему не отгружают и место на складе держат с таймаутом. Считать «платёж создан» за «деньги получены» — самая частая ошибка в этом месте.
- Результат приходит уведомлением, а не в ответе. Покупатель вернётся не всегда: закроет вкладку, потеряет сеть. Итог платежа приносит уведомление от провайдера (webhook), и обработчик этого уведомления обязан быть идемпотентным: одно и то же уведомление приходит повторно, это нормальная работа провайдера.
- Таймаут и сверка. Платёж, который висит в «ждёт подтверждения» дольше нескольких минут, дожимают запросом статуса у провайдера, а не оставляют висеть. Раз в сутки — сверка своих платежей с выпиской провайдера: расхождение ловят так, а не по жалобам.
Отдельный случай — повторные списания по подписке. Первое списание идёт с участием покупателя и с подтверждением, дальнейшие делает сам магазин по сохранённому токену, и они помечаются как операции, инициированные магазином. Провайдеру это сообщают явно: без пометки банк ждёт подтверждения, которого никто не даст, и списание отклоняется.
Требования: что они значат для кода
Формально стандарт — 12 требований; для разработчика они сводятся к нескольким привычкам:
- Не хранить лишнего. PAN — только при доказанной необходимости, зашифрованный при хранении; SAD — никогда. При показе номер маскируют, и «первые 6 и последние 4» — это максимум, который разрешено показать, а не рекомендуемая маска: если в интерфейсе хватает четырёх последних цифр, показывайте только их. Первая часть номера, кстати, не всегда шестизначная — банковский префикс бывает и восьмизначным, так что правило формулируют как «не больше, чем префикс и последние четыре». Проверьте самое коварное место — логи: PAN в логе запроса или в стектрейсе — готовое нарушение. Это тот же принцип, что и с PII в логах.
- Шифрование при передаче: TLS на всех путях карточных данных, включая внутренние.
- Минимальный доступ: роли по принципу «нужно для работы», уникальные учётки (никаких общих админских), MFA для доступа в CDE.
- Журналирование: кто, когда и к каким карточным данным обращался — аудит, защищённый от подмены.
- Безопасная разработка: ревью кода, управление уязвимостями и зависимостями, тестовые среды без прод-данных карт.
В коде это выражается в модели данных: сущность платежа хранит токен, а не номер — тогда PAN негде случайно засветить (в логах, трейсах, дампах БД):
// В сервисе заказов нет ни поля pan, ни cvv — только суррогаты от провайдера.
record Payment(String token, String brand, String last4, Instant paidAt) {}
// Оплата: номер ушёл провайдеру, к нам вернулся токен.
paymentRepo.save(new Payment("tok_1a2b3c", "VISA", "1111", Instant.now()));
// Повторное списание (подписка, доплата) — по токену, не по номеру:
gateway.charge("tok_1a2b3c", amount);
// В сервисе заказов нет ни поля pan, ни cvv — только суррогаты от провайдера.
type Payment struct {
Token string
Brand string
Last4 string
PaidAt time.Time
}
// Оплата: номер ушёл провайдеру, к нам вернулся токен.
payments.Save(ctx, Payment{Token: "tok_1a2b3c", Brand: "VISA", Last4: "1111", PaidAt: time.Now()})
// Повторное списание (подписка, доплата) — по токену, не по номеру:
gateway.Charge(ctx, "tok_1a2b3c", amount)
// В сервисе заказов нет ни поля pan, ни cvv — только суррогаты от провайдера.
type Payment = { token: string; brand: string; last4: string; paidAt: Date };
// Оплата: номер ушёл провайдеру, к нам вернулся токен.
await paymentRepo.save({ token: 'tok_1a2b3c', brand: 'VISA', last4: '1111', paidAt: new Date() });
// Повторное списание (подписка, доплата) — по токену, не по номеру:
await gateway.charge('tok_1a2b3c', amount);
# В сервисе заказов нет ни поля pan, ни cvv — только суррогаты от провайдера.
@dataclass(frozen=True)
class Payment:
token: str
brand: str
last4: str
paid_at: datetime
# Оплата: номер ушёл провайдеру, к нам вернулся токен.
payment_repo.save(Payment("tok_1a2b3c", "VISA", "1111", datetime.now(timezone.utc)))
# Повторное списание (подписка, доплата) — по токену, не по номеру:
gateway.charge("tok_1a2b3c", amount)
Если PAN всё же проходит транзитом (собственная форма), для показа его маскируют. Ниже — предельный допустимый вариант: видны префикс и последние четыре, остальное скрыто.
static String maskPan(String pan) {
return pan.replaceAll("(?<=\\d{6})\\d(?=\\d{4})", "*");
}
// 4111111111111111 -> 411111******1111
В регулярных выражениях Go нет просмотра назад, поэтому маску собирают срезами строки:
func maskPan(pan string) string {
return pan[:6] + strings.Repeat("*", len(pan)-10) + pan[len(pan)-4:]
}
// 4111111111111111 -> 411111******1111
const maskPan = (pan) => pan.replace(/(?<=\d{6})\d(?=\d{4})/g, '*');
// 4111111111111111 -> 411111******1111
def mask_pan(pan: str) -> str:
return re.sub(r"(?<=\d{6})\d(?=\d{4})", "*", pan)
# 4111111111111111 -> 411111******1111
А главное — PAN и CVV не должны попадать в объекты, которые уходят в логи и в тексты ошибок: их там ищут в первую очередь.
Номер карты живёт только у провайдера, к вам приезжает токен с брендом и последними четырьмя цифрами, и повторное списание идёт по этому же токену.
Токен провайдера — тоже ключ к деньгам
Токенизация описана выше как способ вывести номер карты из своих систем, и это правда. Но из неё легко сделать неверный вывод: раз токен «суррогат без ценности для вора», значит с ним можно обращаться как с обычным идентификатором. Нельзя: токеном списывают деньги. Это не номер карты, а ключ, который вы сами и принимаете.
Отсюда правила, которые в статьях про токенизацию обычно опускают:
- Токен не попадает в логи, адреса и события. Он живёт в платёжном сервисе, в одной таблице, и наружу выходит только как последние четыре цифры и тип карты.
GET /payments?token=tok_…— та же ошибка, что номер карты в строке запроса. - Списание — внутренняя операция. Ручка «списать по токену» не должна быть доступна клиенту ни под какой ролью: иначе клиент, подобрав чужой токен, платит с чужой карты. Списание инициирует ваш сервис, а клиент видит только заказ.
- Область действия токена суживают у провайдера. Большинство провайдеров умеют выдавать токен, действующий однократно, или ограниченный только повторными списаниями по подписке. Для разового платежа берут однократный, для подписки — привязанный к этому назначению, а не универсальный.
- Удаление клиента отзывает токен. Удалить строку в своей базе недостаточно: пока токен действует у провайдера, по нему можно списать. Отзыв делается вызовом к провайдеру и входит в ту же процедуру удаления данных, что и остальные шаги.
- Ручка списания под лимитом. Скорость вызовов ограничивают, а всплеск отказов от провайдера считают сигналом: перебор токенов выглядит именно так.
Тестовые среды и тестовые карты
«Тестовые среды без прод-данных карт» — строка из списка требований, за которой скрывается самая дорогая ошибка эксплуатации: боевой дамп, восстановленный в тестовый контур. В момент восстановления тестовый контур становится частью CDE со всеми требованиями, только без их выполнения — там нет ни сегментации, ни ограниченного доступа, а пароли обычно общие.
Что делают вместо этого.
Тестовые карты провайдера. У любого провайдера есть набор номеров для проверок: карта, которая всегда оплачивается, карта, которая всегда отклоняется, карта, которая требует подтверждения 3DS, карта с недостатком средств. Они не секретны, их можно и нужно держать в репозитории рядом с тестами — это единственные номера карт, которым место в коде.
Отдельные ключи и явный признак среды. Ключи тестового и боевого режима у провайдера разные, и их легко перепутать; тестовый ключ в проде выглядит особенно неприятно — платежи «проходят», а денег нет. Поэтому среда видна в самом ключе и проверяется при старте: сервис с тестовым ключом и признаком production не поднимается.
Восстановление с обезличиванием. Если тестовые данные всё же берут из боевых, то не копией, а срезом с обезличиванием, и делает это сама процедура восстановления, а не человек по памяти. Колонку с токенами при этом вычищают: токен — боевой ключ к деньгам, в тестовом контуре ему делать нечего.
Проверка, что дамп не приехал. Полезен простой ночной контроль в тестовой базе: поиск строк, похожих на номер карты (тринадцать-девятнадцать цифр, проходящих проверку по алгоритму Луна), и сравнение числа клиентов с ожидаемым для тестовых данных. Он ловит и восстановленный дамп, и разработчика, который «залил немного реальных данных, чтобы проверить».
Если номер карты всё же попал в лог
Это происходит, и почти всегда одинаково: кто-то залогировал тело запроса, чтобы разобраться с отказом провайдера. Дальше важен порядок: сначала перекрыть, потом чистить, потом сообщать.
Перекрыть. Убрать запись и выкатить, а не «поправить в следующем релизе». Пока строка пишется, любая чистка бессмысленна.
Понять, куда разошлось. Строка в логе не одна: она в горячем индексе хранилища логов, в его репликах, в холодном архиве, в резервных копиях самого хранилища, в трассировке, в отчёте об ошибке, который фронтенд отправил в свою систему, в тикете со скриншотом и на ноутбуке у того, кто копировал лог себе. Чистят по списку, а не по памяти, и удаляют индексы целиком, если выборочно не получается.
Сообщить. У стандарта есть требование иметь план реакции на инцидент, и его смысл в том, чтобы решение не принимал один разработчик в пятницу. О случившемся сообщают ответственному за соответствие и банку-эквайеру; скрыть и промолчать хуже по последствиям, чем сам инцидент. Отдельно проверяют, что утекло: если в лог попали CVV или содержимое полосы, это нарушение запрета хранить такие данные вообще, и разбор будет жёстче, чем по номеру карты.
Закрыть на будущее. Одного «больше так не делать» мало, потому что делать будет другой человек. Фильтр маскирования в самом журнальном приложении (шаблон на последовательности цифр с проверкой по алгоритму Луна), запрет логировать тело на платёжных путях, тест на маскирующую функцию и периодический поиск номеров в логах как контрольная мера — вот что действительно закрывает.
Глубже: уровни и проверкирасширенное
Глубина проверки зависит от объёма операций: от ежегодного самоопросника (SAQ) у малых до аудита на площадке сертифицированным аудитором (QSA) у крупных. Пороги задаёт каждая платёжная система отдельно и подтверждает банк-эквайер, поэтому точную цифру всегда уточняют у него. Порядок величин такой: у Visa и Mastercard верхний уровень начинается примерно с 6 млн операций по картам этой системы в год, следующий — с миллиона, а подавляющее большинство интернет-магазинов попадает в нижний уровень, где хватает самоопросника.
Практическое следствие для команды важнее самих порогов: какой именно SAQ вы заполняете, определяется не оборотом, а архитектурой приёма платежей — редирект на провайдера, его iframe или собственная форма дают разные списки требований. Решение «как принимаем карты» стоит принимать вместе с тем, кто отвечает за соответствие, — до реализации, а не после.
Глубже: российский контекст: НСПК, МИР и 152-ФЗрасширенное
Сам стандарт создан международными платёжными системами, а операции по картам внутри России обрабатываются через Национальную систему платёжных карт: и карты международных систем, выпущенные в России, и карты «МИР», для которых НСПК ещё и платёжная система. Практически это значит, что требования к вам приходят не от Visa напрямую, а от банка-эквайера и от правил НСПК, и формулировки могут отличаться от того, что написано в англоязычной версии стандарта.
Отсюда два практических следствия. Первое: перед выбором способа приёма платежей требования уточняют у эквайера — какой опросник он ждёт, какие документы просит, требует ли 3DS обязательно. Второе: у карт «МИР» свои тонкости в потоке подтверждения и в наборе тестовых карт, поэтому провайдера выбирают из тех, кто работает и с ними, а тестовые сценарии прогоняют на карте каждой системы отдельно.
И пересечение с законом. Номер карты сам по себе — не персональные данные в смысле 152-ФЗ, но он никогда не лежит в одиночестве: рядом фамилия держателя, телефон, адрес доставки, и всё вместе подпадает под закон целиком. Значит, к платёжным данным применяются оба списка требований сразу: правила стандарта — к номеру и подтверждающим данным, закон — к персональным данным вокруг платежа, включая требование первичного хранения в России. Выбор платёжного провайдера поэтому не только про удобство API: он определяет, где окажутся данные ваших покупателей.
Коротко
- PCI DSS обязателен для всех, кто хранит, обрабатывает или передаёт карточные данные; это договорное требование платёжных систем со штрафами через банк.
- CVV и прочие SAD после авторизации не хранятся нигде — ни в базе, ни в кэше, ни в логах; PAN — только при необходимости, зашифрованный, а при показе не больше префикса и последних четырёх. Самое частое нарушение — номер карты в логах.
- Главный ход — сузить CDE: токенизация через платёжного провайдера выводит ваши сервисы из области аудита; при собственной обработке — жёсткая сегментация.
- Для кода это: минимум данных, TLS везде, наименьшие привилегии, аудит доступа, чистые логи и тестовые среды без прод-карт.
- Способ приёма платежей (redirect / iframe / своя форма) определяет глубину аудита — выбирайте его осознанно и заранее.
- 3-D Secure переносит ответственность за мошенническую операцию на банк-эмитента, а в коде делает платёж двухшаговым: статус «ждёт подтверждения», идемпотентный обработчик уведомления, таймаут и ночная сверка с выпиской.
- Токен провайдера — ключ к списанию, а не безобидный суррогат: не в логи и не в адреса, ручка списания только внутренняя, при удалении клиента токен отзывают у провайдера.
- В тестовой среде только тестовые карты и тестовые ключи; боевой дамп не восстанавливают, а если берут срез — обезличивают и вычищают токены.
- Номер в логе: сначала убрать запись и выкатить, потом вычистить индексы, архивы и копии, потом сообщить эквайеру; закрывает это маскирующий фильтр с проверкой по алгоритму Луна и тест на него.
- В России операции идут через НСПК, требования приходят от эквайера, а персональные данные вокруг платежа остаются под 152-ФЗ вместе с требованием первичного хранения в стране.
Что почитать дальше
- PII и секреты — как не размазывать чувствительные данные по логам и конфигам.
- Аудит действий администраторов — журналирование, которого требует и PCI DSS.
- Где какая проверка — слои авторизации вокруг платёжных операций.