Module Federation на примере фишинга
- среда, 24 декабря 2025 г. в 00:00:13
Разберём микрофронтенд через историю вымышленного хакера — и заодно поймём, почему это спрашивают на собеседованиях.
Статья собрана по заметкам в телеграм канале.
Недавно на собесе меня спросили: "А как именно работают микрофронты? Там что, прямо eval используют?"
Я что-то промямлил про expose, host, сборку... и понял, что вообще не понимаю сути. Знакомо?
После этого я потратил неделю на изучение webpack изнутри. Чтобы запомнить материал, придумал историю про горе-хакера.
Это образовательная статья. Фишинг — это уголовка.
Иван Донов Джон Доу на информатике услышал про "фишинг", "XSS", а главное про "крипту"! После пятёрки за год он считает себя крутым хацкером. Его амбициозный план — взломать несколько учёток GitHub, закинуть в репозитории майнер крипты и стричь капусту.
Джон не дурак: зачем верстать с нуля, если можно взять настоящий GitHub?
Джон узнал про старую технологию <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'Независимые команды, независимая раскатка, загрузка по сети.
Джон узнал, что webpack умеет загружать код с других серверов. Новый план: найти LoginForm на GitHub и подгрузить к себе.
Когда приложение делает import('loginApp/LoginForm'), webpack проходит 4 шага:
const script = document.createElement('script');
script.src = 'https://github.com/remoteEntry.js';
document.head.appendChild(script);// Содержимое remoteEntry.js (упрощённо):
window.loginApp = {
init: function(hostShared) {
// Синхронизация зависимостей (React, etc.)
this._shared = hostShared;
},
get: function(moduleName) {
// Загрузка конкретного модуля
return import('./chunks/' + moduleName + '.js');
}
};await window.loginApp.init({
react: {
'18.2.0': { get: () => import('react') }
}
});
// "Эй, у меня React 18.2.0, используй его"const LoginForm = await window.loginApp.get('./LoginForm');
// Броузер загружает и выполняет модуль// 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.
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;Джону нужно подключить 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>Джон реализовал свой план. Но почему в реальности этого бы не случилось?

GitHub никогда не вернёт:
Access-Control-Allow-Origin: https://гитхаб.рфДаже если бы CORS не было, GitHub использует CSP:
Content-Security-Policy: script-src 'self' github.githubassets.comЭто значит: "Выполняй только скрипты с github.com и github.githubassets.com".
Хитрый метод проверки хэшей микрофронтов.
Module Federation — способ делиться модулями между независимыми webpack-сборками
Remote — тот, кого подгружают
Host — тот, кто подгружает
remoteEntry.js — файл с функциями init() и get()
Shared dependencies — общие библиотеки (React), чтобы не грузить дважды
Код встраивается в броузер через <script> – всё безопасно
Подписывайтесь на канал в Telegram — там разбираю JavaScript и фронтенд для подготовки к собеседованиям.