Найкращі практики обфускації JavaScript

Як за замовчуванням постачати обфускований JavaScript, позначати директивами важливі ділянки і постійно змінювати збірку, щоб зловмисники не могли вас випередити.

Обфускація має бути стандартом для вебзастосунків, що йдуть у продакшн

Усе, що є у вашому опублікованому JavaScript, можна прочитати. Секрети, бізнес-логіку, алгоритм, на який ви покладаєтеся: будь-хто може відкрити бандл, а передова LLM на запит пояснить його рядок за рядком. Сканер безпеки показує, що саме витягується з реального продакшн-бандла. Запустіть його на своєму, щоб побачити, що ви вже віддаєте.

Обфускація цього бандла дає конкретні властивості, які можна перевірити:

  • Патч, написаний під ваш застосунок, перестає переживати ваші релізи. Юзерскрипт, скрипт для Tampermonkey чи розширення, яке налаштовує або автоматизує ваш застосунок, мусить чіплятися за щось стабільне: ім'я функції, рядковий літерал, впізнавану форму. Перейменування і кодування рядків прибирають ці якорі, а новий сід змінює форму результату в кожній збірці. Автор пише патч під один реліз; з наступним він уже не збігається, і користувачі, яким автор його роздав, лишаються зі зламаним скриптом, доки його не перепишуть.
  • Пошуку із заміною у вашому коді нема за що зачепитися. Проста текстова заміна, найдешевший спосіб автоматизувати чи змінити вебзастосунок, працює з isSubscribed і з "Upgrade to continue". Жодного з цих рядків немає у файлі, який ви постачаєте.
  • Ваші константи перестають бути читабельним текстом. Літерали закодовано, тож grep ключа, ендпоінта, таблиці цін чи тексту навколо перевірки доступу нічого не повертає, як і сканери та скрейпери, що масово читають бандли.
  • Результат — не ваша програма. AfterPack переписує логіку в еквівалентні форми, які знищують інформацію. Той, хто розбере артефакт, відновить робочу програму, але не ту, яку написали ви, і алгоритмічних рішень, через які вашу варто було копіювати, там уже немає.
  • Витрати повторюються. Немає структури, незмінної між збірками, на яку можна націлитися, тож деобфускатор чи патч, написаний під минулий реліз, не переноситься на цей, а вже виконана кимось робота не переноситься далі.

Ніщо з цього не є гарантією і не замінює повноважень на боці сервера. Будь-який клієнтський код можна тією чи іншою мірою відновити, бо середовище виконання мусить його запускати. Змінюються ціна й частота: той, хто хоче прочитати чи змінити ваш застосунок, платить за це знову в кожному вашому релізі. Решта сторінки про те, як витратити цей захист там, де він важить.

Змінюйте збірку, щоб випереджати зловмисників

Один обфускований реліз — це нерухома ціль, до якої обхідник капчі, розширення-чит чи скрейпер можуть пристосуватися, якщо докласти достатньо зусиль. Новий сід у кожній збірці діє за замовчуванням, тож кожен реліз уже структурно відрізняється від попереднього, і той, хто пристосувався до одного, починає заново на наступному.

Якщо ви випускаєте релізи рідко, між ними цей захист затихає. Додайте перезбирання за розкладом, незалежне від змін у коді, щоб розгорнутий бандл усе одно змінювався; розділ Збірки та CI показує, як вбудувати це в конвеєр.

Оберіть пресет, а потім позначте важливе

Пресет за замовчуванням — light (цільова складність 5): він кодує рядкові літерали, перейменовує ідентифікатори та переписує синтаксис і не додає структурних шарів. Підвищте його глобально для продакшн-збірки, якій потрібен справжній захист:

$ npx afterpack@latest dist/ --preset=hard

Шкала: minify (0), light (5), medium (7), hard (12), extreme (25); див. Пресети. --complexity=<number> перевизначає цільове значення пресету: --preset=hard --complexity=40 — це hard із цільовим значенням 40. Значення, задане так, застосовується до кожного токена, навіть на light.

Потім позначте ділянки, де помилка коштує найдорожче, щоб важча обробка потрапила туди, де вона виправдовує свою ціну в розмірі.

Позначте ділянку директивою

/* @afterpack preset=hard */
function checkLicense(token: string): boolean {
  return verifySignature(token, PUBLIC_KEY);
}
/* @afterpack end */

const apiKey = /* @afterpack preset=extreme */ "YOUR_SECRET";
// only the rest of this line is covered

Директиви — це довідник: граматика, кожен ключ, який може задати директива, що позначати і чому, і що відбувається з директивою у збірці, яка не може її застосувати. Перевизначення для ділянок діють у збірках Pro.

Дві трансформації захисту від втручання

Захист від втручання не входить до preset=hard. Це два явні прапорці конфігурації Pro, обидва за замовчуванням вимкнені й не застосовуються на Free:

  • transforms.comparisonHardening.enabled переписує строге порівняння з рядковим літералом часу збірки (x === "LIT") на односторонню перевірку дайджесту, тож літерала в артефакті взагалі немає. Використовуйте його для будь-якої перевірки, що порівнює з константою, яку ви не хочете віддати grep.
  • transforms.selfIntegrity.enabled додає перевірку цілісності обфускованого результату. Якщо файл не змінено, вона ніяк не впливає на поведінку. Змінений файл не декодується коректно. Їй потрібен код рівня medium або вище, до якого можна прикріпитися, тож збірка на light містить її лише всередині ділянки, піднятої до такого рівня. Починаючи з рушія 0.2.3, збірка без такого коду завершується помилкою DIAG_SELF_INTEGRITY_INERT, а не постачається без перевірки.

Задавайте обидва для всієї програми. Див. Конфігурація.

Захистіть ліцензію з прив'язкою до домену чи пробний період

AfterPack не має вбудованої трансформації прив'язки до домену чи терміну дії. Напишіть перевірку самі, а AfterPack захистить написаний вами JavaScript. Для перевірки ліцензії на домен із терміном пробного періоду:

/* @afterpack preset=hard */
if (location.hostname !== "customer-domain.example.com") return;
if (Date.now() > TRIAL_EXPIRES_AT) return;
/* @afterpack end */

Коли увімкнено transforms.comparisonHardening.enabled, порівняння location.hostname стає перевіркою дайджесту, тож дозволений хост ніколи не з'являється в артефакті. grep за доменом нічого не знаходить. preset=hard ставить навколишню логіку на цільову складність 12 з увімкненим нижнім порогом для констант, а новий сід змінює її форму в кожному релізі.

Це підвищує ціну й частоту патчингу перевірки; перевірку з поверхні атаки це не прибирає. Наполегливий зловмисник, який запускає код, усе ще може знайти й пропатчити порівняння, але мусить повторювати цю роботу в кожному релізі. Сприймайте це як перешкоду для зловживання пробним періодом і поєднуйте із серверною перевіркою прав доступу для будь-якої перевірки, від якої залежить реальний дохід.

Не витрачайте захист на сторонній код

Бандли постачальників, як-от React, Lodash чи рантайм CSS-in-JS, мало що виграють від захисту. Вони й так публічні, роздувають ваш бандл і часто використовують рефлексію під час виконання, яку потрібно явно дозволити.

`paths.exclude` виключає на рівні файлів

paths.exclude повністю пропускає відповідний файл ще до розбору і повертає його байт у байт. Про виключений файл повідомляється як DIAG_PATH_EXCLUDED. Це працює на рівні файлів: коли збірка бібліотеки об'єднує ваш код і залежність в один вихідний файл, окремого файлу для виключення немає. Натомість використовуйте identifiers.reserved.

Чотири способи обрати, що бачить AfterPack:

  • Виключіть файл через paths.exclude, наприклад ["**/*.min.js", "dist/vendor/**"]. * і ? збігаються в межах одного сегмента шляху, цілий сегмент ** збігається з нулем чи більше сегментів, а зіставлення чутливе до регістру.
  • Спрямуйте CLI на каталог, який хочете захистити. Він рекурсивно обходить каталог збірки і бере кожен знайдений .js/.mjs/.cjs, пропускаючи вкладені node_modules/ (включіть їх через paths.include) і власні копії .backup.<hash>. Чанк постачальника поза цим каталогом ніколи не зачіпається.
  • Винесіть код постачальників в окремий чанк у вашому бандлері, а потім запускайте AfterPack на каталозі, який його не містить. Чанк постачальника нормально обфускується, якщо ви готові прийняти ціну.
  • Позначте ділянки як skip там, де код залежності з бандла лежить в одному файлі з вашим і має залишатися впізнаваним.

Ваші імпорти й далі розв'язуються.

AfterPack повністю підтримує приватні поля й методи класів ES2022: #field і #method() коректно розбираються, перейменовуються й захищаються, як і все інше.

Збережіть те, що має залишатися читабельним

Деякий код мусить зберігати свої оригінальні імена, щоб середовище виконання працювало. Пресети для фреймворків покривають поширені випадки. Для решти зберігайте вибірково. Ніколи не вимикайте перейменування глобально, інакше ви втратите найдешевший шар захисту в усьому бандлі.

СитуаціяЩо робити
Публічні експорти, які викликачі імпортують за іменем/* @afterpack skip */ … /* @afterpack end */ навколо експорту або закріпіть імена через identifiers.reserved
Стектрейси, які ви справді налагоджуєтеСимволізуйте їх за source map, які не потрапляють у дерево розгортання. identifiers.rename=false зберігає кожен ідентифікатор без змін у всьому бандлі
DI на основі рефлексії (Angular, NestJS, Inversify)reflection.allow у конфігурації, щоб явно дозволити власний патерн; не дозволений явно патерн є помилкою збірки
instanceof / розгалуження за іменем класуidentifiers.reserved з іменами класів або /* @afterpack skip */ навколо оголошення
Символи стороннього API, зібрані в один файл із вашим кодом (наприклад, бібліотечна збірка Rollup/Vite, яка вбудовує залежність)identifiers.reserved, зі звичайним іменем для всієї збірки або записом { glob, names } для відповідних файлів
Скрипт, у який ніщо інше не викликаєidentifiers.globals.rename: true. Вмикає перейменування навколишньої глобальної поверхні, яку рушій інакше зберігає. За замовчуванням вимкнено, і це безпечно лише тоді, коли ніщо зовнішнє не читає ці імена

identifiers.reserved зберігає лише перелічені вами імена, де б вони не траплялися, і зіставляє їх з авторським іменем на рівні вихідного коду до будь-якого перейменування: точно, за цілим ідентифікатором, з урахуванням регістру. Доступно на Free. Використовуйте його, коли не можете позначити цілу ділянку, бо ваш код і вбудована залежність лежать в одному файлі.

Щоб зберегти імена лише в одному місці, використовуйте identifiers.reserved або /* @afterpack skip */; див. Що можна задати.

Кожне збереження знижує захист на цих рядках. Застосовуйте його лише там, де цього вимагає середовище виконання.

Стратегія сіду

Сід визначає форму обфускованого результату. Випадковий сід для кожної збірки діє за замовчуванням. Залишайте так для продакшну: збірка, яка змінюється з кожним релізом, є основним захистом.

Закріплюйте сід рівно в одному місці: у CI, коли потрібен відтворюваний результат для влучань у кеш чи тестів на еталонних знімках.

$ npx afterpack@latest dist/ --seed=git

--seed=git виводить сід із git rev-parse HEAD. Якщо репозиторію немає, він переходить на новий випадковий сід і друкує повідомлення, а не завершує збірку помилкою. Так результат детермінований для кожного коміту і водночас змінюється з кожним релізом.

Сід також має змінну середовища AFTERPACK_seed з тими самими значеннями, що й --seed. Збірки з кількома бандлами автоматично використовують спільний сід, у межах процесу і між процесами через цю змінну. Задавайте її самі лише для відтворення конкретної збірки; явний --seed або параметр seed усе одно мають пріоритет над нею.

Зберіть продакшн один раз і просувайте ті самі байти через qa → preprod → prod. Dev-збірки не обфускуються, тож щоденна розробка не зачіпається.

Не випускайте в продакшн сталий сід. Він не дає результату змінювати форму між релізами.

Тримайте артефакти поза розгортанням

AfterPack записує три види локальних артефактів. Кожен із них чутливіший за бандл, який він описує.

  • Protection Map (protectionMap.html) містить ваш оригінальний вихідний код. Вона завжди записується у внесений до gitignore каталог .afterpack/ у корені проєкту, а ніколи не поруч зі збіркою, яку роздає сервер, і продакшн-збірка попереджає, коли вона увімкнена. Вимкніть її через --protectionMap.enabled=false.
  • Резервна копія ваших оригіналів. Перш ніж переписати файл на місці, CLI копіює оригінал у .afterpack/backup/ разом із маніфестом того, у що він обфускував кожен файл. Цей каталог — ваш вихідний код як він є, він залишається на машині збірки, а npx afterpack@latest restore повертає оригінали на місце. Вимкніть це через --build.backup=false. Плагін для фреймворку за замовчуванням копії не пише; якщо там задати build.backup як true, натомість поруч із результатом записується сусідній файл .backup.<hash>, і там він потрапить у реліз разом зі збіркою.
  • Source map пов'язують обфускований результат напряму з оригінальним вихідним кодом. Сусідній файл .map за замовчуванням вимкнений у продакшні, і коментар //# sourceMappingURL= за замовчуванням у продакшні теж вимкнений.

Плагіни та CLI не допускають .afterpack/ у git за допомогою .gitignore усередині нього, ніколи не редагують ваш власний і попереджають, коли артефакт потрапляє під шлях, який роздає сервер. Файл .map або .backup.<hash> поруч зі збіркою ви маєте самі не допускати в git.

Ніколи не розгортайте source map чи резервну копію

Source map або резервна копія містить увесь ваш вихідний код. Ніколи не розгортайте ні те, ні інше.

@afterpack/next також видаляє кожен .js.map із дерева, яке роздає сервер, після збірки.

Налагоджуйте продакшн-стектрейси, не публікуючи карту

Продакшн-збірка не записує ні .map, ні коментаря //# sourceMappingURL=, і це правильна поведінка за замовчуванням, яка змушує вас символізувати стектрейси вручну. Якщо вашій команді потрібні справжні стектрейси з продакшну, збирайте карту і тримайте її приватною, замість того щоб відмовлятися від неї.

Увімкніть карту для збірки й вимкніть вказівник на неї, щоб ніщо в опублікованому JavaScript не повідомляло, що карта існує:

{
  "sourceMap": { "enabled": true, "emitUrl": false, "sourcesContent": true }
}

Потім тримайте файли .map поза каталогом розгортання: завантажуйте їх у свій трекер помилок або зберігайте як артефакт CI і символізуйте там. Якщо ваше розгортання копіює весь каталог збірки, додатково забороніть ці файли на edge, щоб запит до будь-якого з них ніколи його не повертав.

location ~ \.map$ {
  return 404;
}

Таке саме правило має відповідник усюди: правило CDN чи WAF для *.map, deny у проксі або бакет сховища, який просто ніколи не отримує ці файли. Перевірте це ззовні вашої мережі через curl -I https://yoursite.example.com/assets/app.js.map, очікуючи 404, і через npx afterpack@latest audit, який повідомляє про доступну карту як про знахідку.

Правило заборони — друга лінія захисту, а не перша

Правило на edge захищає файли, які лежать на сервері. Усе, що його обходить, чи то неправильно налаштований маршрут, пряма URL-адреса бакета чи прев'ю-розгортання, віддає карту з вашим оригінальним вихідним кодом усередині. Не розгортати карти взагалі — це варіант, який неможливо налаштувати неправильно.

Якщо ви випадково випустили секрет

API-ключ, токен чи пароль, що потрапив у ваш бандл, — це активний інцидент. AfterPack змінює те, що читач знаходить в опублікованому файлі. Він не змінює того факту, що облікові дані вже вийшли назовні.

Що AfterPack із ним робить. На пресеті за замовчуванням рядкові літерали кодуються, тож ключ, що зберігається в такому літералі, не можна прочитати у файлі, який ви розгортаєте. Ті кілька літералів, що завжди залишаються читабельними, перелічено на сторінці Пресети. Пошук у бандлі ключа чи тексту навколо нього нічого не повертає. Автоматичні сканери секретів і скрейпери бандлів теж нічого не знаходять, а саме так виявляють більшість злитих ключів.

Чого AfterPack не робить. Облікові дані все ще є у збірці, і ваш код усе ще їх використовує. Будь-хто, хто запускає ваш застосунок у браузері під своїм контролем, може дістатися значення в момент, коли код його використовує. Кодування дає вам час відреагувати. Воно не робить клієнтський секрет безпечним.

Що робити, по порядку:

  1. Виконайте ротацію ключа. Вважайте його скомпрометованим із моменту, коли збірка вийшла. Саме цей крок закриває інцидент.
  2. Перенесіть повноваження на сервер. Нехай облікові дані тримає ваш власний бекенд, і він вирішує, чи може викликач отримати результат. Ключ, якого браузер ніколи не отримує, не може витекти.
  3. Перевірте, що зараз у продакшні. Запустіть npx afterpack@latest audit на своєму розгорнутому сайті. Він повідомляє про розкриті секрети, злиті source map і рівень захисту для JavaScript, який сайт фактично роздає, тож ви дізнаєтеся, що читач може витягти з продакшну просто зараз.
  4. Перезберіть і перерозгорніть, щоб бандл зі старим значенням перестав роздаватися.
Обфускація не замінює ротацію

Облікові дані, що вийшли в реліз, потребують ротації, яким би пресетом не була зібрана збірка.

Тестуйте обфусковану збірку

Завжди запускайте інтеграційні тести на обфускованій збірці. Тестування збірки до обфускації нічого не каже про те, що йде в реліз.

Збій на обфускованій збірці, коли звичайна проходила, вказує або на прогалину в збереженні (ім'я, яке очікувало середовище виконання, було перейменовано), або на нерозпізнаний патерн рефлексії. Діагностика зазвичай називає ключ конфігурації, який це виправляє. Запустіть із --diagnostics.level=all, щоб побачити кожну діагностику повністю замість згорнутого підсумку.

Зелена збірка — це і є весь сигнал

Усе, що випустило б слабший захист, ніж ви налаштували, натомість зупиняє збірку, зокрема файл, який рушій не може обфускувати, і збірка Pro, яка не може дістатися хмари. Контракт кодів виходу та кожен код за ним описано в розділі Діагностика.

Далі

  • Збірки та CI: життєвий цикл із просуванням тих самих байтів, гігієна артефактів і періодичні перезбирання.
  • Модель загроз: шкала супротивників, під яку відкалібровано ці директиви.
  • Конфігурація: кожен ключ конфігурації в одному місці.
  • Директиви: повна граматика маркерів ділянок, що стоїть за таблицею вище.
  • Пресети: п'ятирівнева шкала, яку ці директиви перевизначають для ділянок.
  • Protection Map: перевірте, який клас насправді застосовано до позначених ділянок.
  • Збірки в дашборді: ті самі числа, для кожної збірки, для ваших хмарних збірок Pro.