Skip to content

Iframe и 3-D Secure ​

English version: iframe.md

<iframe> показывает одну веб-страницу внутри другой. 3-D Secure так и работает: страница банка грузится внутри нашей страницы оплаты. Звучит просто, но как только две страницы с разных сайтов (разные origin), браузер ставит между ними стену ради безопасности. Этот документ объясняет стену: что делает каждая настройка, зачем она и как это используется в проекте.

Если запомнить только одно: родительская и встроенная страницы управляют разными вещами и не могут читать внутренности друг друга.


Две стороны стены ​

mermaid
flowchart LR
    subgraph parent["Наша страница (родитель)"]
      A["ставит на &lt;iframe&gt;:<br/>sandbox, referrerpolicy, allow"]
    end
    subgraph frame["Встроенная страница (банк)"]
      B["ставит через HTTP-заголовки:<br/>CSP frame-ancestors, X-Frame-Options,<br/>Set-Cookie SameSite/Secure, COOP/COEP/CORP"]
    end
    A -- "postMessage (проверь origin!)" --> B
    B -- "postMessage (проверь origin!)" --> A
  • Родитель решает, что фрейму вообще позволено (через атрибуты тега <iframe>).
  • Встроенная страница решает, кому можно её встраивать и как ведут себя её куки (через HTTP-заголовки ответа).
  • Пока они cross-origin, ни одна не лезет в DOM другой. Общение - только через postMessage.

Что ставит родитель (атрибуты <iframe>) ​

В проекте: apps/demo/src/features/checkout/ui/three-ds-challenge.tsx.

sandbox ​

Закрытый фрейм, которому возвращают только перечисленные возможности.

Флаг, который мы даёмЧто разрешаетЗачем
allow-scriptsвыполнять JavaScriptстранице банка это нужно
allow-formsотправлять формычтобы отправить OTP
allow-same-originсохранить свой originчтобы работали её куки и postMessage имел настоящий event.origin

Флаги, которые мы намеренно не даём: allow-top-navigation (фрейм не должен уметь увести всё окно), allow-popups, allow-modals.

mermaid
flowchart LR
    subgraph box["🔒 Sandbox-фрейм (по умолчанию всё заперто)"]
      G1["✅ allow-scripts"]
      G2["✅ allow-forms"]
      G3["✅ allow-same-origin"]
      D1["🚫 top-navigation"]
      D2["🚫 popups"]
      D3["🚫 modals"]
    end

Известная ловушка: allow-scripts + allow-same-origin вместе опасны только если встроенная страница того же origin, что и родитель - тогда она могла бы снять свой же sandbox. Здесь банк на другом origin, поэтому allow-same-origin просто оставляет ему origin банка. Власти над нашей страницей это фрейму не даёт.

Альтернатива: убрать allow-same-origin совсем. Тогда фрейм получает "непрозрачный" origin - без кук, а его сообщения приходят с event.origin === "null", что ломает проверку origin. Поэтому для cross-origin банка мы его оставляем.

referrerpolicy="no-referrer" ​

Не сообщать банку, с какой страницы пришёл пользователь. Альтернатива: strict-origin шлёт только origin, без полного URL.

allow (Permissions Policy) ​

Выдаёт фрейму конкретные возможности браузера (камера, автозаполнение OTP, WebAuthn и т.д.). Здесь ничего не нужно, поэтому опускаем. Реальные банковские SDK иногда добавляют allow="otp-credentials; publickey-credentials-get" для автозаполнения OTP и passkeys.

target у формы ​

Скрытая форма постит в фрейм с target="acs-frame" (режим iframe). Та же форма с target="_top" отправляет всё окно в банк - это режим redirect (ниже).


Что ставит встроенная страница (HTTP-заголовки) ​

В проекте: apps/bank-sim/src/acs/lib.ts (securityHeaders).

Подробный разбор каждого заголовка простыми словами: security-headers.md.

ЗаголовокЧто делаетАльтернатива
Content-Security-Policy: frame-ancestors <origin>кто может встраивать страницу (антиclickjacking)X-Frame-Options (устаревший)
X-Frame-Options: ALLOW-FROM / DENY / SAMEORIGINстарая версия того же; современные браузеры игнорируют ALLOW-FROMиспользуйте frame-ancestors
Set-Cookie: ...; SameSite=None; Secureразрешает куке жить в third-party фрейме (нужен https)SameSite=Lax/Strict = нет cross-site куки
Cross-Origin-Opener-Policy: same-originизолирует группу browsing-contextunsafe-none - выключить
Cross-Origin-Embedder-Policy: require-corpтребует, чтобы субресурсы явно соглашалисьunsafe-none
Cross-Origin-Resource-Policy: cross-originразрешает другому origin встроить этот ответsame-origin - заблокировать
X-Content-Type-Options: nosniffзапрет MIME-sniffing-

Строка с кукой - то, на чём спотыкаются: фрейм с другого сайта это "third-party" контекст, и современные браузеры выкидывают его куки, если они не SameSite=None; Secure. Secure значит только https - поэтому ACS-сервер работает по TLS.


Разговор через стену: postMessage ​

Cross-origin страницы не могут читать переменные и DOM друг друга (same-origin policy). Единственный канал - window.postMessage. Две вещи держат его безопасным:

  1. Когда получаешь сообщение, проверь event.origin по белому списку. Само сообщение ничего не доказывает - фейковый "success" может прислать кто угодно.
  2. Когда шлёшь, указывай явный target origin, никогда "*".
js
// приём (родитель)
window.addEventListener('message', (event) => {
  if (event.origin !== ACS_ORIGIN) return // доверяем только банку
  // ...считаем подсказкой, затем подтверждаем на бэкенде
})

Золотое правило для платежей: сообщение - только подсказка. Итог всегда берётся с бэкенда (здесь authenticate() перепроверяет статус). Сообщение можно подделать, ответ сервера - нет.

mermaid
flowchart TD
    M1["postMessage от банка<br/>event.origin = банк ✅"] -->|"origin ок → берём как подсказку"| CHK
    M2["postMessage от evil-site<br/>event.origin = атакующий 🚫"] -->|"origin не прошёл → игнор"| DROP["🛑 отброшено"]
    CHK{"затем спрашиваем бэкенд"} -->|"бэкенд подтвердил"| PAID["✅ реально оплачено"]
    CHK -->|"бэкенд не согласен"| NO["🚫 не оплачено<br/>(сообщение подделали)"]

Iframe против перенаправления всей страницы ​

Банки делают 3-D Secure в одной из двух форм, и проект поддерживает обе.

IframeПеренаправление всей страницы
Где показывает банкво фрейме внутри страницывся страница уходит в банк
Сигнал возвратаpostMessage в родителябанк шлёт 302 на return-URL
Твоё состояниесохраняется (без перезагрузки)стирается (полная перезагрузка) - неси нужное в URL
Кукиthird-party (нужен SameSite=None; Secure)first-party на странице банка
frame-ancestorsваженне задействован (top-level страница)

Последняя строка - и есть причина, зачем существуют оба: во фрейме банк это third-party (строгие правила кук и антиclickjacking); при перенаправлении всей страницы (top-level) он first-party, и правила смягчаются.


Как это связано в проекте ​

  • Родитель / UI: apps/demo/src/features/checkout/ui/three-ds-challenge.tsx - сам <iframe>, его sandbox/referrerpolicy, две формы (iframe и redirect) и слушатель message с проверкой origin.
  • Встроенный "банк": apps/bank-sim/src/acs/ - отдельный https-сервер, который ставит заголовки выше и в режиме redirect делает 302 на /3ds/return. См. apps/bank-sim/README.md.
  • Общая картина платежей: architecture.md.

Дополнительно почитать ​

Apache-2.0 · Headless-движок, адаптеры провайдеров и запускаемые примеры.