Перейти до вмісту

Основи безпеки

Змінні оточення Vite відкриті: префікс означає опублікувати це

Змінні оточення Vite відкриті у вашому застосунку, бо так просив префікс. VITE_ і NEXT_PUBLIC_ означають опублікувати це, а ШІ, який їх додав, не знав ціни.

Vlad Tkachenko9 хв читання
Файл .env із трьома значеннями, два з них із міткою VITE_, і ті самі два значення, намальовані ще раз у вікні браузера поруч.

Коротко

  • Змінні оточення Vite відкриті у вашому застосунку, бо саме цього просив префікс. Змінна з назвою VITE_ або NEXT_PUBLIC_ копіюється в JavaScript, який завантажує кожен відвідувач, і те, що вона лежить у файлі .env, цього не зупиняє.
  • Префікс є правильною міткою для адреси чи публічного ключа і неправильною для всього, що витрачає гроші або ігнорує правила вашої бази даних. Збірка не відрізняє одне від іншого, і ШІ, який написав цей рядок, теж.
  • Із 30 998 живих vibe-coded застосунків, які ми просканували, 1 332 віддавали щось схоже на ключ. 1 142 з них були ключами Google API, яким зазвичай потрібне обмеження, а не ротація. 52 віддавали ключ, який витрачає гроші або читає все.

Хтось відкрив ваш застосунок на Lovable, натиснув F12 і знайшов у коді значення, яке ви точно поклали у файл .env. Або ви попросили в білдера нову функцію, він написав рядок, що починається з VITE_, а щось, прочитане вами відтоді, каже, що саме через цей префікс витікають ключі. У назві файлу є .env, і кожен посібник каже ніколи ним не ділитися. Змінні оточення Vite, відкриті кожному відвідувачу, звучать як помилка у Vite.

Ось та частина, яку посібник за посібником пояснює неправильно: ніщо не провалило спробу сховати це значення. VITE_ і NEXT_PUBLIC_ є інструкцією для вашого інструмента збірки, і ця інструкція звучить так: поклади значення в застосунок, який завантажує кожен відвідувач. Інструмент прочитав мітку і зробив те, що на ній написано.

Збірка пакує ваш застосунок у коробку, і кожен відвідувач отримує копію цієї коробки. Префікс є міткою на одному значенні, яка каже: запакуй і це. Решта цієї статті про те, для яких значень ця мітка правильна, чому ШІ чіпляє її на неправильні, і як побачити, що лежить у вашій власній коробці.

Чи відкриті змінні оточення Vite для відвідувачів?

Ті, що починаються з VITE_, так, і саме для цього префікс існує.

Vite, інструмент збірки за більшістю застосунків на Lovable і Bolt, читає ваш файл .env під час кожної збірки. Змінна з назвою VITE_SUPABASE_URL копіюється в бандл, стиснутий файл JavaScript, який завантажує кожен відвідувач, і там ваш код читає її як import.meta.env.VITE_SUPABASE_URL. Змінна з назвою DB_PASSWORD, без префікса, приходить у браузер порожньою. Власна документація Vite каже, що значення з префіксом під час збірки вбудовуються у ваш вихідний код і не повинні містити ключів API.

Next.js, на якому будує v0, має те саме правило під іншою назвою: NEXT_PUBLIC_. Його документація описує значення як inlined, жорстко вписаний рядок, записаний у браузерний бандл під час збірки. Expo використовує EXPO_PUBLIC_ і попереджає тими самими словами. Старіші проєкти Create React App використовують REACT_APP_. Кожен префікс каже своєму інструменту збірки те саме: це йде в коробку.

Чому файл .env здається приватним, хоч він не такий

Бо звичка тримати ключ у файлі .env прийшла із серверів, де вона працює.

На сервері файл і код, який його читає, лежать на машині, яку контролюєте ви. Відвідувач отримує від цієї машини відповідь і ніколи не бачить файлу. Винести ключ із коду у файл, який сервер читає під час запуску, там є доброю практикою, і саме там написано кожен посібник, який каже «поклади це в .env».

У вашого файлу .env є два читачі, а посібники розповідають про одного. Перший читач: будь-хто, хто може бачити ваш проєкт, тобто співавтор, публічний репозиторій на GitHub, перегляд файлів у самому білдері. Запис у .gitignore, тобто у списку файлів, які git пропускає, тримає .env подалі від цього читача. Другий читач: збірка, яка відкриває файл щоразу, коли ви публікуєте, і копіює звідти все, що має префікс. .gitignore їй нічого не каже.

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

Тож «мій .env у gitignore» є правдою, і це відповідь на інше запитання. Файл лишився поза вашим репозиторієм. Значення з префіксом усе одно потрапили в застосунок, бо саме цей шлях префікс і відкриває, а в проєкті на Lovable, Bolt чи v0 більшість коду, який ви редагували, працює в браузері, де немає сервера, за яким файл міг би залишитися.

Чому ШІ потягнувся до префікса

Бо саме так змушують значення працювати в коді браузера, а модель гадки не має, скільки це значення коштує.

Ви попросили мапу або чат, який відповідає на запитання про ваш продукт. Код, який білдер для цього написав, працює в браузері відвідувача, а код браузера, що читає process.env.OPENAI_API_KEY, не отримує нічого. Спосіб доставити значення: префікс. Білдер перейменовує змінну на VITE_OPENAI_API_KEY, функція працює в попередньому перегляді, і ніде не з’являється жодної помилки, бо з боку інструмента збірки нічого не пішло не так.

Префікс не судить. Це та сама інструкція і для ключа Google Maps, який має бути публічним після обмеження, і для секретного ключа Stripe, який може повернути гроші кожному вашому клієнту. Людина, яка знала б, що другий списує гроші з вашої картки, зупинилася б. Модель знає, що функція не працювала, доки цього рядка не було, а потім запрацювала.

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

Які значення можуть стояти за префіксом

Адреса і публічний ключ. Усе, що витрачає гроші або ігнорує правила вашої бази даних, ні.

ЗначенняЗа VITE_ чи NEXT_PUBLIC_?Чому
VITE_SUPABASE_URLТут доречноАдреса. Каже, з яким проєктом говорить ваш застосунок, і більше нічого.
VITE_SUPABASE_PUBLISHABLE_KEY, або VITE_SUPABASE_ANON_KEY у старішому проєктіТут доречноСтворений для браузера. Кожен його запит і далі фільтрує Row Level Security, тобто правила на кожній таблиці, які рядок за рядком вирішують, кому що можна читати.
Ключ Stripe pk_live_Тут доречноБудує форми оплати. Не може ані списати, ані повернути гроші, ані прочитати клієнтів.
Ключ Google MapsТут доречно, після обмеженняПублічний за задумом. Обмеження за referrer у Google Cloud і є тим, що не дає чужинцю нарахувати вам витрати з його допомогою.
Ключ OpenAI або AnthropicНіколиПублічного варіанта не існує. Хто його тримає, той витрачає ваші гроші.
Ключ Supabase service_role або sb_secret_НіколиОбходить Row Level Security і читає кожен рядок у кожній таблиці.
Ключ Stripe sk_live_НіколиСписання, повернення, виплати і кожен запис клієнта.
Ключ доступу AWSНіколиУсе, що може цей обліковий запис, звідусіль.

Якщо у вашому застосунку є VITE_SUPABASE_URL і поруч публічний ключ, це та пара, яку Supabase задумав для браузера, і наше сканування позначає її як доречну. Адреса і публічний ключ і є причиною, чому префікс існує.

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

Що 30 998 застосунків віддавали за префіксом

Здебільшого ключі Google API. 52 застосунки віддавали ключ, який витрачає гроші або читає все.

У серпні 2026 року ми запустили ті самі дев’ять перевірок на 30 998 живих vibe-coded застосунках. 1 332 з них, тобто 4%, віддавали щось схоже на ключ у коді, який завантажує кожен відвідувач. 1 142 з них були ключами Google API, яким зазвичай потрібне обмеження, задане в Google Cloud, а не ротація. 204 віддавали випадкове на вигляд значення поруч із назвою на кшталт secret чи password, яке може бути справжніми обліковими даними, а може й ні.

Дорогі випадки були рідкісними. 33 застосунки віддавали ключ OpenAI, 9 ключ доступу AWS, 5 ключ Anthropic, 3 секретний ключ Stripe і 3 ключ service_role від Supabase: 52 застосунки загалом, бо один із них ніс два. Тож значення за префіксом зазвичай є ключем Google, і виправлення для нього є налаштуванням. Рідкісний випадок і є тим, де живе шкода, а скільки коштує витеклий ключ OpenAI, розповідає стаття саме про нього.

Як перевірити, що віддає ваш власний застосунок

Відкрийте живий застосунок, натисніть F12 і пошукайте значення в кожному завантаженому файлі.

  1. Відкрийте свій опублікований застосунок у браузері за його справжньою адресою. Попередній перегляд у білдері є іншою збіркою і може відставати на версію.
  2. Натисніть F12, щоб відкрити інструменти розробника, і виберіть вкладку Sources.
  3. Натисніть Ctrl+Shift+F або Cmd+Option+F на Mac. Це відкриє пошук по кожному файлу, який завантажила сторінка.
  4. Вставте перші приблизно десять символів значення, яке вас непокоїть, і більше нікуди його не вставляйте.

Збіг означає, що значення лежить у коробці, яку отримує кожен відвідувач. Шукайте значення, а не назву: збірка зазвичай замінює import.meta.env.VITE_OPENAI_API_KEY на саме значення, тож пошук за VITE_ може повернутися порожнім, тоді як кожне значення за ним на місці.

Збірка замінює назву на значення. Пошук VITE_ у живому застосунку не знаходить нічого; пошук значення знаходить його.

Якщо ви не хочете проходити свій застосунок значення за значенням, наше безкоштовне сканування читає ваш живий сайт ззовні, усі дев’ять перевірок, приблизно за 20 секунд і без облікового запису. Воно називає кожен знайдений ключ за типом, каже, які з них доречні в браузері, і пише «Не вдалося перевірити» для всього, на що не змогло відповісти, замість галочки: проскануйте свій застосунок.

Куди натомість іде справжній секрет

На машину, з якої ваші відвідувачі ніколи нічого не завантажують. У проєкті Supabase це Edge Function, невеликий шматок серверного коду, який Supabase запускає за вас; у застосунку на Next.js це серверний маршрут; у застосунку на Replit це серверна половина.

Форма всюди та сама. Ваш код у браузері просить вашу функцію зробити роботу. Функція тримає ключ, робить виклик до OpenAI чи Stripe і повертає відповідь. Ключ лишається на машині, а відвідувач отримує результат. Стаття про OpenAI малює це як один об’єкт, що зсувається на одну клітинку праворуч, і в цьому вся зміна.

Дві речі скажуть вам, що білдер зробив те, про що ви просили. Змінна втратила префікс, тож тепер це OPENAI_API_KEY, і живе вона у власних секретах функції, заданих у панелі Supabase у розділі Edge Functions, а в .env застосунку нічого немає. І файл, який її читає, лежить у supabase/functions/ або app/api/, там, куди збірка ніколи не пакує, а не в src/.

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

У проєкті на Replit той самий поділ має власну назву, Secrets, і що інструмент Secrets покриває, а що ні, є окремою статтею.

Що робити просто зараз

Що робити

  • Пошукайте у своєму проєкті VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ і REACT_APP_. Кожен збіг є значенням, яке ваша збірка публікує навмисно. Для кожного вирішіть, чи може воно бути публічним.
  • Залиште адресу і публічний ключ. VITE_SUPABASE_URL і VITE_SUPABASE_PUBLISHABLE_KEY, або ключ anon у старішому проєкті, є тією парою, заради якої префікс існує.
  • Секретний ключ за префіксом спершу проходить ротацію в провайдера, а потім переноситься. Видалення рядка не повертає копію, яку вже завантажили.
  • Перенесіть роботу, якій був потрібен ключ, в Edge Function або серверний маршрут, із ключем у секретах цієї функції і без префікса в назві.
  • Обмежте ключ Google за referrer у Google Cloud. Йому потрібне налаштування, а значення він зберігає.
  • Після наступної публікації пошукайте в живому застосунку кожне значення, яке ви перенесли.

Кожна публікація пакує нову коробку. Наступна функція, яку ви попросите, є ще однією нагодою для значення отримати префікс, і ніщо між білдером та інтернетом не читає бандл на виході. Сканування минулого місяця прочитало бандл минулого місяця.

Reeve Monitor читає бандл за вас. Він щогодини повторює всі дев’ять перевірок на щонайбільше трьох застосунках, повідомляє вас, коли результат змінюється, стежить за доступністю кожні 60 секунд і надсилає щомісячний звіт. Ключ, який потрапляє в бандл із публікацією у вівторок, є в повторному скануванні тієї самої години, згадали ви подивитися чи ні. Коштує $12 на місяць за прайсом, із сімома безкоштовними днями до першого списання; сторінка цін іноді нижча за цифру тут і ніколи не вища.

Якщо вам зручніше пройти це списком, 10-хвилинний чекліст безпеки покриває це і решту речей, які варто вимкнути в щойно запущеному застосунку.

Поширені запитання

Чи є файли .env секретними?

Від вашого репозиторію так, якщо файл записаний у .gitignore. Від ваших відвідувачів ні. Збірка читає .env щоразу, коли ви публікуєте, і копіює кожне значення з префіксом VITE_ або NEXT_PUBLIC_ у JavaScript, який віддає ваш застосунок. Сам файл ніколи не залишає вашу машину; значення, які ви позначили для браузера, залишають.

Чи безпечно відкривати VITE_SUPABASE_ANON_KEY?

Він створений, щоб бути там. Ключ anon, який у новішому проєкті зветься публічним ключем, розрахований на життя в браузері. Він лише каже, до якого проєкту належить запит, і кожен запит, який він робить, фільтрують ваші правила Row Level Security. Це тримається рівно доти, доки ці правила ввімкнені й правильні, а це окрема перевірка.

Чи NEXT_PUBLIC_ чимось відрізняється від VITE_?

Те саме правило, інший інструмент збірки. Next.js під час збірки записує значення кожної змінної NEXT_PUBLIC_ у браузерний бандл як жорстко вписаний рядок, а змінна без префікса приходить у код браузера порожньою. Expo робить те саме з EXPO_PUBLIC_, а старіші проєкти Create React App із REACT_APP_. Хоч би який інструмент зібрав ваш застосунок, префікс означає опублікувати.

Як перевірити, що є в моєму бандлі?

Відкрийте живий застосунок, натисніть F12, виберіть Sources і натисніть Ctrl+Shift+F (Cmd+Option+F на Mac), щоб шукати в кожному файлі, який завантажила сторінка. Вставте перші кілька символів значення. Шукайте значення, а не назву змінної, бо збірка зазвичай замінює назву на значення, тож VITE_ може бути відсутнім, тоді як ключ на місці. Наше безкоштовне сканування робить те саме читання ззовні приблизно за 20 секунд.

Де має жити секретний ключ у застосунку на Lovable чи Bolt?

У Supabase Edge Function, із ключем, заданим у секретах цієї функції в панелі Supabase, і без префікса VITE_ у назві. Ваш код у браузері викликає функцію, функція викликає провайдера з ключем, і ключ ніколи не дістається відвідувача. Якщо ключ уже пішов у бандл, зробіть ротацію в провайдера, перш ніж переносити його.

Чи захищає .gitignore мої ключі?

Він тримає файл .env поза git, тож ніхто, хто читає ваш репозиторій, його не бачить. На збірку він не впливає: вона читає файл напряму й публікує кожне значення з префіксом. Файл .env у .gitignore з VITE_OPENAI_API_KEY усередині все одно віддає цей ключ кожному відвідувачу.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Не впевнені, як справи у вашому застосунку?

Запустіть безкоштовне сканування й отримайте зрозумілу оцінку від A до F приблизно за 20 секунд. Без облікового запису й без картки.

Сканувати безкоштовно

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.