← Назад в базу знаний

Как антифрод Stripe выявляет прокси по TLS-отпечатку JA4 при оплате подписок

Что вообще такое 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 будет вообще ни на что не похож, и это красный флаг сам по себе.

✔️Купить прокси