javascript

Module Federation на примере фишинга

  • среда, 24 декабря 2025 г. в 00:00:13
https://habr.com/ru/articles/979242/

Разберём микрофронтенд через историю вымышленного хакера — и заодно поймём, почему это спрашивают на собеседованиях.

Статья собрана по заметкам в телеграм канале.


Недавно на собесе меня спросили: "А как именно работают микрофронты? Там что, прямо eval используют?"

Я что-то промямлил про expose, host, сборку... и понял, что вообще не понимаю сути. Знакомо?

После этого я потратил неделю на изучение webpack изнутри. Чтобы запомнить материал, придумал историю про горе-хакера.

Это образовательная статья. Фишинг — это уголовка.


Пролог

Иван Донов Джон Доу на информатике услышал про "фишинг", "XSS", а главное про "крипту"! После пятёрки за год он считает себя крутым хацкером. Его амбициозный план — взломать несколько учёток GitHub, закинуть в репозитории майнер крипты и стричь капусту.

Джон не дурак: зачем верстать с нуля, если можно взять настоящий GitHub?


iframe

Джон узнал про старую технологию <iframe>. Идея простая: встроить настоящий GitHub и перехватить данные.

<!-- гитхаб.рф/index.html -->
<html>
  <body>
    <iframe src="github.com/login"></iframe>
  </body>
</html>

Почему не работает?

Форма внутри iframe отправляет данные напрямую на GitHub. Броузер не даёт JavaScript доступ к содержимому iframe с другого домена (правила Same-Origin Policy).

const loginFrame = document.querySelector("iframe");
const form = loginFrame.contentDocument.querySelector("form");
// ❌ Uncaught DOMException: Blocked a frame with origin "гитхаб.рф"
// from accessing a cross-origin frame.

Историческая справка

Представь: у тебя super app — одно приложение с кучей фич. Звонки, календарь, чаты, платежи. Некий аналог приложения российского Т-банк, китайского WeChat или арабского Telegram. Как разбить это на команды?

Эволюция подходов

iframe:

<iframe src="https://gifts-team.telegram.com"></iframe>
<iframe src="https://ton-team.telegram.com"></iframe>
<iframe src="https://stickers.telegram.com"></iframe>

Разбили, но как обеспечить обмен данными между ними? (сложно).

Монорепозиторий:

import Gifts from "../teams/gifts/Gifts";
import Ton from "../teams/ton/Ton";
import Stickers from "../teams/stickers/Stickers";

function App() {
  return (
    <>
      <Gifts />
      <Ton />
      <Stickers />
    </>
  );
}

Git конфликты, долгая сборка проекта – катим всё или ничего.

Module Federation:

import Stickers from 
'stickers-team@https://stickers-team.telegram.com/remoteEntry.js'

Независимые команды, независимая раскатка, загрузка по сети.


Module Federation

Джон узнал, что webpack умеет загружать код с других серверов. Новый план: найти LoginForm на GitHub и подгрузить к себе.

Как работает Module Federation под капотом

Когда приложение делает import('loginApp/LoginForm'), webpack проходит 4 шага:

Шаг 1: Создаётся

const script = document.createElement('script');
script.src = 'https://github.com/remoteEntry.js';
document.head.appendChild(script);

Шаг 2: remoteEntry.js регистрируется в window

// Содержимое remoteEntry.js (упрощённо):
window.loginApp = {
  init: function(hostShared) {
    // Синхронизация зависимостей (React, etc.)
    this._shared = hostShared;
  },

  get: function(moduleName) {
    // Загрузка конкретного модуля
    return import('./chunks/' + moduleName + '.js');
  }
};

Шаг 3: Host вызывает init()

await window.loginApp.init({
  react: {
    '18.2.0': { get: () => import('react') }
  }
});
// "Эй, у меня React 18.2.0, используй его"

Шаг 4: Host вызывает get()

const LoginForm = await window.loginApp.get('./LoginForm');
// Броузер загружает и выполняет модуль

Конфигурация webpack

// webpack.config.js хакера
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "phishingShell",
      remotes: {
        githubLogin: "githubLogin@https://github.com/remoteEntry.js"
      }
    })
  ]
};
// App.jsx хакера
const LoginForm = React.lazy(() => import("githubLogin/LoginForm"));

function App() {
  const handleCredentials = (username, password) => {
    // Джон мечтает прочитать данные здесь...
  };

  return <LoginForm onSubmit={handleCredentials} />;
}

Чего-то не хватает

Джон запустил сервер, открыл страницу и... стили формы поехали. Тема не подхватилась. Не похоже на гитхаб ☹️

Проблемы

  • LoginForm ожидает CSS стили от родительского приложения.

  • LoginForm использует ThemeContext для определения темы.

Module Federation хорошо работает когда каждому компоненту известен контракт – набор правил, которых от него ожидают.

Исправления

Чтобы микрофронтенд заработал, Джону нужно эмулировать родительское приложение – GitHub.

  1. ThemeContext ожидается как shared-зависимость.

Создаём контекст:

import { createContext } from "react";

export const ThemeContext = createContext("light");
export default ThemeContext;

Добавляем ThemeProvider:

import React from "react";
import { ThemeContext } from "githubLogin/ThemeContext";

function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

export default ThemeProvider;
  1. Джону нужно подключить CSS-фреймворк, который использует GitHub.

<html>
  <head>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
    />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

А что, так можно было?

Джон реализовал свой план. Но почему в реальности этого бы не случилось?

1. CORS

GitHub никогда не вернёт:

Access-Control-Allow-Origin: https://гитхаб.рф

2. Content Security Policy

Даже если бы CORS не было, GitHub использует CSP:

Content-Security-Policy: script-src 'self' github.githubassets.com

Это значит: "Выполняй только скрипты с github.com и github.githubassets.com".

3. Subresource Integrity

Хитрый метод проверки хэшей микрофронтов.


Практические выводы для собесов:

  1. Module Federation — способ делиться модулями между независимыми webpack-сборками

  2. Remote — тот, кого подгружают

  3. Host — тот, кто подгружает

  4. remoteEntry.js — файл с функциями init() и get()

  5. Shared dependencies — общие библиотеки (React), чтобы не грузить дважды

  6. Код встраивается в броузер через <script> – всё безопасно


Код с примерами


Литература


Подписывайтесь на канал в Telegram — там разбираю JavaScript и фронтенд для подготовки к собеседованиям.