Как антифрод Stripe выявляет прокси по TLS-отпечатку JA4 при оплате подписок
Содержание
- Что вообще такое JA4 и откуда он взялся
- Почему именно подписки и именно Stripe
- Как Stripe это использует в реальности
- Что показывает JA4 у типичных прокси-стеков
- Почему socks5 не спасает
- HTTP/2 fingerprint — второй слой
- Реальные грабли при оплате подписок
- Что делать, если вы легальный мерчант
- Практика: как проверить свой стек перед боем
- Итог по архитектуре
Что вообще такое JA4 и откуда он взялся
JA4 — это эволюция JA3. Джон Альтман из FoxIO выкатил его в 2023-м, потому что JA3 сломался. Точнее, сломался ещё раньше, но в 2023-м это признали официально.
JA3 хешировал список шифров, расширений, эллиптических кривых и форматов точек. Проблема: любое обновление Chrome меняло порядок расширений, хеш прыгал, и все, кто строил на JA3 правила, каждые пару месяцев переписывали базы. Плюс GREASE (RFC 8701) вносил случайные значения, которые надо было вырезать.
JA4 считает иначе. Он не хеширует, а формирует читаемую строку из нескольких полей, разделённых подчёркиваниями. Формат такой:
```
t13d1516h2_8daaf6152771_e5627efa2ab1
```
Первая часть — метаданные: `t` (TCP), версия TLS (13), наличие SNI (d = domain), количество шифров (15), количество расширений (16), ALPN (h2 = HTTP/2). Дальше — SHA-256 от отсортированного списка шифров и расширений. Сортировка — ключевой момент. Порядок расширений больше не важен, GREASE вырезается автоматически.
Для антифрода это подарок. Потому что теперь клиент можно идентифицировать не по IP (который меняется каждую минуту), а по тому, как его TLS-стек договаривается о соединении.
Почему именно подписки и именно Stripe
Stripe обрабатывает платежи по подпискам через Billing. Каждый рекуррентный платёж — это запрос к API с токеном карты. Мерчант видит только `payment_intent.succeeded` или `charge.failed`. Но Stripe видит больше: он видит IP, с которого пришёл запрос на создание PaymentIntent, TLS-отпечаток этого соединения, User-Agent, Accept-Language, порядок заголовков.
Для кардера подписка — идеальный сценарий. Первый платёж на \$1–5 проходит проверку 3DS, дальше списывается автоматически месяцами. Если карта умирает, кардер подставляет новую. Прокси нужен, чтобы IP не совпадал с домашним и не палил страну.
И вот тут начинается охота. IP-репутация — штука дешёвая, резидентские прокси продаются пачками. А TLS-отпечаток подделать сложнее. Точнее, можно, но об этом ниже.
Как Stripe это использует в реальности
Stripe не публикует детали своего антифрода. Но по косвенным признакам (Radar rules, поведение при разных конфигурациях клиента) картина такая.
При создании PaymentIntent Stripe собирает fingerprint соединения: JA4, порядок HTTP/2-фреймов, набор поддерживаемых шифров, наличие TLS 1.3 session tickets, размер ClientHello. Плюс классические сигналы — IP, ASN, гео, часовой пояс из JavaScript-фрейма (Stripe.js шлёт свои данные отдельно от серверного запроса).
Дальше идёт сопоставление. Если с одного IP за час приходит десять PaymentIntent с JA4, который в базе помечен как `python-requests` или `curl`, — это флаг. Если JA4 совпадает с реальным Chrome, но HTTP-заголовки говорят `User-Agent: Mozilla/5.0 (Windows NT 10.0)`, а TLS-стек — это Go net/http, — второй флаг.
Ключевое: Stripe сравнивает JA4 серверного запроса с JA4, который присылает Stripe.js из браузера. Обычно это один и тот же клиент. Когда расходятся — сигнал.
Что показывает JA4 у типичных прокси-стеков
Прокси не меняет TLS-отпечаток сам по себе. Если вы гоните трафик через SOCKS5, TLS-рукопожатие делает ваш клиент. Значит, JA4 будет от клиента.
Проблема в том, что большинство автоматизаторов используют не браузер, а библиотеку.
| Клиент | JA4 (пример) | TLS | ALPN |
|---|---|---|---|
| Chrome 124 | `t13d1516h2_8daaf6152771_...` | 1.3 | h2 |
| Firefox 125 | `t13d1715h2_5b57614c22b0_...` | 1.3 | h2 |
| curl 8.7 (OpenSSL) | `t13d1516h2_...` (близко к Chrome) | 1.3 | h2 |
| python-requests | `t13d1517h2_...` (OpenSSL, без GREASE) | 1.3 | h2 |
| Go net/http | `t13d1516h2_...` (свой порядок шифров) | 1.3 | h2 |
Видите? curl и Go выглядят похоже на Chrome по метаданным, но хеш шифров и расширений отличается. Потому что Chrome добавляет GREASE, поддерживает определённый набор curves, шлёт `application_settings` extension (ALPS), `compress_certificate`, `signed_certificate_timestamp`. Go этого не делает.
Проверить свой отпечаток можно так:
```bash
curl -s https://tls.peet.ws/api/all | jq '.tls.ja4'
```
Или через Python:
```python
import requests
r = requests.get("https://tls.peet.ws/api/all")
data = r.json()
print("JA4:", data["tls"]["ja4"])
print("JA3:", data["tls"]["ja3"])
print("User-Agent sent:", data["http1"]["headers"]["user-agent"])
```
Если вы гоните через прокси lexic.ml с резидентским IP, но шлёте запрос из python-requests — JA4 выдаст вас с потрохами. IP чистый, а стек — библиотечный.
Почему socks5 не спасает
SOCKS5 работает на уровне 4. Он туннелирует TCP-поток. TLS-рукопожатие происходит между вашим клиентом и сервером Stripe (или Cloudflare перед ним). Прокси видит только зашифрованные байты. Отпечаток формируется на вашей стороне.
То же с HTTP-прокси. Даже если прокси переписывает заголовки, TLS уже случился.
Единственный способ спрятать JA4 — терминировать TLS на прокси и делать новое рукопожатие от имени прокси. Это MITM, и для этого нужен либо свой CA-сертификат на клиенте, либо прокси, который сам прикидывается браузером. Второе называется TLS-impersonation.
Инструменты: `curl-impersonate`, `curl_cffi` (Python), `bogdanfinn/tls-client` (Go). Они воспроизводят ClientHello Chrome или Firefox побайтово.
```python
from curl_cffi import requests
r = requests.get(
"https://api.stripe.com/v1/payment_intents",
impersonate="chrome124",
headers={"Authorization": "Bearer sk_test_..."}
)
print(r.json())
```
Вот тут JA4 будет совпадать с Chrome. Но появляется другая проблема — TCP/IP-отпечаток (p0f), порядок HTTP/2-фреймов и timing. Об этом ниже.
HTTP/2 fingerprint — второй слой
JA4 ловит TLS. Но Stripe общается по HTTP/2. И у HTTP/2 есть свой отпечаток: порядок фреймов SETTINGS, значения параметров (INITIAL_WINDOW_SIZE, MAX_FRAME_SIZE), приоритеты потоков, наличие PRIORITY-фреймов.
Chrome шлёт SETTINGS с конкретными значениями: `HEADER_TABLE_SIZE=65536`, `MAX_CONCURRENT_STREAMS=1000`, `INITIAL_WINDOW_SIZE=6291456`. Python httpx шлёт дефолты из h2-библиотеки: `INITIAL_WINDOW_SIZE=65535`. Разница видна сразу.
FoxIO добавил в JA4 поле для HTTP/2 — называется JA4H. Формат похож: метод, версия, наличие cookie, referer, количество заголовков, порядок заголовков.
Если вы используете curl_cffi с impersonate — HTTP/2-отпечаток тоже подделывается. Если просто requests с прокси — нет.
Реальные грабли при оплате подписок
Разберу три сценария, которые ломаются на практике.
Первый — скрипт на Python с requests через резидентский прокси. IP чистый, гео совпадает, но JA4 — это OpenSSL из python-requests. Stripe Radar помечает как `card_testing` или `fraudulent` с вероятностью выше порога. PaymentIntent создаётся, но 3DS-челлендж не проходит, либо платёж уходит на manual review. Решение: перейти на curl_cffi с impersonate Chrome, синхронизировать User-Agent, Accept-Language и часовой пояс.
Второй — Node.js с axios. Тот же расклад. Node использует OpenSSL, JA4 отличается от Chrome. Плюс axios шлёт заголовки в другом порядке, чем браузер. JA4H это ловит. Решение: использовать `puppeteer` с реальным Chrome или `node-tls-client`.
Третий — Selenium с headless Chrome. Тут JA4 правильный, но headless-режим меняет другие сигналы: `navigator.webdriver`, отсутствие плагинов, размер окна. Stripe.js это видит через JS-фрейм, и хотя серверный JA4 чистый, общий скор падает.
Что делать, если вы легальный мерчант
Если вы не кардер, а обычный разработчик и ваши платежи ловятся — проверьте, не гоните ли вы серверные запросы к Stripe через прокси или VPN. Иногда CI/CD или бэкенд в дата-центре даёт IP из ASN хостинга, и Stripe это видит.
Проверка простая:
```bash
curl -s https://api.stripe.com/v1/charges \
-u sk_test_YOUR_KEY: \
-H "Stripe-Version: 2024-06-20"
```
Если ответ приходит, но Radar ставит `blocked` — смотрите на IP. Датацентровые ASN (DigitalOcean, Hetzner, AWS) в антифрод-базах помечены. Резидентский прокси решает проблему IP, но не решает проблему JA4, если вы шлёте из библиотеки.
Правильный подход для бэкенда: не притворяться браузером. Stripe для серверных запросов не требует браузерного JA4. Он смотрит на API-ключ, на IP, на паттерн запросов. Если вы шлёте 10 000 запросов в час с одного IP — это нормально для крупного мерчанта, но подозрительно для мелкого.
Практика: как проверить свой стек перед боем
Соберите скрипт, который покажет всё, что видит Stripe.
```python
from curl_cffi import requests
import json
session = requests.Session(impersonate="chrome124")
r = session.get("https://tls.peet.ws/api/all")
data = r.json()
print(json.dumps({
"ja4": data["tls"]["ja4"],
"ja4h": data["http2"].get("akamai_fingerprint"),
"user_agent": data["http1"]["headers"]["user-agent"],
"accept_language": data["http1"]["headers"].get("accept-language"),
"ip": data["ip"],
}, indent=2))
```
Запустите через ваш прокси. Сравните JA4 с реальным Chrome (откройте tls.peet.ws в браузере). Если совпадает — хорошо. Если нет — вы палитесь.
Дальше проверьте HTTP/2-фреймы. Akamai fingerprint должен совпадать с Chrome. У curl_cffi с impersonate это работает. У обычного requests — нет.
И последнее — timing. Браузер шлёт ClientHello через 10–50 мс после TCP-рукопожатия. Скрипт может шлёгнуть через 200 мс, если делает что-то между. Stripe это не всегда ловит, но в комбинации с другими сигналами — да.
Итог по архитектуре
JA4 — это не серебряная пуля. Это один из слоёв. Stripe комбинирует TLS-отпечаток, HTTP/2-отпечаток, IP-репутацию, поведенческие сигналы из Stripe.js, историю карты, BIN-географию.
Прокси решает IP. Не решает JA4. Чтобы решить JA4, нужен TLS-impersonation на клиенте. Чтобы решить HTTP/2 — impersonation на уровне библиотеки. Чтобы решить поведенческие сигналы — реальный браузер или очень хорошая эмуляция.
Для легальных задач (тестирование, автоматизация своих платежей) хватает curl_cffi с impersonate и чистого IP. Для кардерских — не хватает ничего, потому что Stripe обновляет правила каждые несколько недель, а сообщество impersonation-инструментов не успевает.
Если вы гоните трафик через прокси-сервис, убедитесь, что он не подменяет TLS на своей стороне. Некоторые дешёвые прокси делают MITM и ломают отпечаток — тогда JA4 будет вообще ни на что не похож, и это красный флаг сам по себе.