انتقل إلى المحتوى

دليل

مفتاح API الخاص بـ LLM موجود على الأرجح في حزمة الواجهة الأمامية

لماذا تنتهي متغيرات VITE_ وNEXT_PUBLIC_ في المتصفح، وكيف تفحص البناء بحثًا عن مفتاح OpenAI أو Anthropic مسرَّب، ومسار الخادم الذي يحلّ المشكلة.

· قراءة في 7 دقائق · بقلم فريق vetkit

مفتاح كبير معروض داخل نافذة متصفح زجاجية؛ روبوت يلمّع الزجاج غافلًا، بينما يشير مطوّر بفزع ويد مرتدية قفازًا تمتد نحو المفتاح.مفتاح كبير معروض داخل نافذة متصفح زجاجية؛ روبوت يلمّع الزجاج غافلًا، بينما يشير مطوّر بفزع ويد مرتدية قفازًا تمتد نحو المفتاح.

تطلب من مساعد ذكي إضافة مربع محادثة إلى تطبيق React الخاص بك. فيثبّت OpenAI SDK، ويقرأ المفتاح من import.meta.env.VITE_OPENAI_API_KEY، وتعمل الميزة من المحاولة الأولى. المشكلة أنها تعمل أيضًا لأي شخص يفتح أدوات المطوّر في المتصفح، فيجد المفتاح في شيفرة JavaScript لديك ويبدأ بإرسال الطلبات على حسابك.

هذا واحد من أكثر الأخطاء شيوعًا التي نراها في المشاريع المكتوبة بمساعدة الذكاء الاصطناعي، ومن السهل أن يفوت في المراجعة لأن لا شيء يبدو خاطئًا. فالمفتاح ليس مكتوبًا مباشرةً في الشيفرة، بل يعيش في ملف .env يتجاهله git بشكل صحيح. والشيفرة تقرؤه من متغير بيئة، وهذا ما ينصح به الجميع.

كيف يحدث ذلك

أدوات بناء الواجهة الأمامية لا تملك متغيرات بيئة وقت التشغيل. فالمتصفح يشغّل حزمة ثابتة. لذا حين تقرأ شيفرتك import.meta.env.VITE_SOMETHING أو process.env.NEXT_PUBLIC_SOMETHING، يستبدل المجمِّع (bundler) هذا التعبير بالقيمة الحرفية أثناء البناء. وبعد البناء لا يعود هناك متغير، بل مجرد سلسلة نصية في ملف JavaScript يُنزّله كل زائر.

وأُطر العمل واضحة في هذا الشأن. تقول Next.js إن المتغيرات العامة تُضمَّن مباشرةً في أي شيفرة JavaScript تُرسل إلى المتصفح. ولا تكشف Vite لشيفرة العميل إلا المتغيرات التي تحمل البادئة VITE_ تحديدًا كي لا تسرّب المتغيرات الأخرى عن طريق الخطأ. فالبادئة هي إعلان الموافقة الصريحة. وحين يختار المساعد VITE_OPENAI_API_KEY لأن الاسم الخالي من البادئة لم يعمل في المتصفح، فقد وافق نيابةً عنك.

وهناك صيغة أهدأ أيضًا. فبعض قوالب البداية تنسخ المفتاح إلى الحزمة باستخدام الخيار define في Vite، مثل 'process.env.GEMINI_API_KEY': JSON.stringify(...) في vite.config.ts. لا يحمل المتغير بادئة عامة، فيبدو آمنًا، لكن الأثر هو نفسه تمامًا.

تحاول حزم SDK الرسمية منع ذلك. فـ OpenAI Node SDK يرفض العمل في المتصفح ما لم تمرّر dangerouslyAllowBrowser: true، ويشرح ملف README الخاص به السبب: فهذا يكشف بيانات اعتمادك السرية في شيفرة جانب العميل. ويضيف المساعدون أحيانًا هذا الخيار للتخلص من رسالة خطأ. أما العملاء الآخرون، مثل استدعاءات HTTP العامة أو حزم SDK التي لا تتحقق من بيئة المتصفح، فلا يحذّرونك على الإطلاق.

أي المتغيرات عامة

إذا بدأ اسم المتغير بإحدى هذه البادئات، فافترض أن قيمته مقروءة لأي شخص يحمّل تطبيقك:

  • VITE_ في Vite (وأطر العمل المبنية عليها)
  • NEXT_PUBLIC_ في Next.js
  • REACT_APP_ في Create React App
  • EXPO_PUBLIC_ في Expo وتطبيقات React Native المبنية بها
  • VUE_APP_ في Vue CLI وGATSBY_ في Gatsby
  • PUBLIC_ في Astro ووحدات متغيرات البيئة العامة في SvelteKit

هذه البادئات مناسبة للقيم المقصود أن تكون عامة، مثل معرّف موقع للتحليلات أو عنوان URL لواجهتك البرمجية. لكنها ليست مناسبة أبدًا لسرٍّ خاص بمزوّد خدمة: OpenAI أو Anthropic أو Google Gemini أو Mistral، أو مفتاح Stripe السري، أو مفتاح service role لقاعدة البيانات.

افحص البناء

أسرع اختبار هو بناء التطبيق والبحث في المخرجات عن بادئة المفتاح. تبدأ مفاتيح مشاريع OpenAI بـ sk-proj-، ومفاتيح Anthropic بـ sk-ant-، ومفاتيح Google API بـ AIza.

bash
npm run build# Vite writes to dist/, Next.js static chunks live in .next/static/grep -rEo "sk-(proj|ant)-[A-Za-z0-9_-]{8}|AIza[0-9A-Za-z_-]{8}" dist .next/static 2>/dev/null

إذا طبع هذا الأمر أي شيء، فالمفتاح موجود في حزمتك. وافحص الموقع المنشور بالطريقة نفسها: افتح أدوات المطوّر، وانتقل إلى تبويب Sources أو Network، وابحث في السكربتات المحمّلة. فإذا استطعت العثور عليه في ثلاثين ثانية، فسيستطيع ذلك أيضًا سكربت يزحف على المواقع بحثًا عن هذا بالضبط.

الحل

انقل استدعاء المزوّد إلى خادم تتحكم فيه، واجعل المتصفح يتواصل مع ذلك الخادم بدلًا منه. في Next.js يكون ذلك route handler. وفي تطبيق Vite أحادي الصفحة يمكن أن يكون دالة serverless على المضيف نفسه. ويحتفظ المفتاح باسم خالٍ من البادئة، فلا يضمّنه المجمِّع أبدًا.

app/api/chat/route.ts
import OpenAI from 'openai'; // Server-only: no NEXT_PUBLIC_ prefix, never shipped to the browser.const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export async function POST(request: Request) {  const user = await requireSignedInUser(request); // your auth check  await rateLimit(user.id); // your limiter: per user, per minute   const { message } = await request.json();  if (typeof message !== 'string' || message.length > 4000) {    return Response.json({ error: 'Invalid message' }, { status: 400 });  }   const response = await client.responses.create({    model: 'gpt-5-mini',    input: message,    max_output_tokens: 800,  });  return Response.json({ reply: response.output_text });}

التفاصيل الثلاثة المحيطة بالاستدعاء لا تقل أهمية عن نقله. فمن دون التحقق من المصادقة يصبح مسارك وكيلًا (proxy) مجانيًا ومجهول الهوية إلى النموذج، وهو التسريب نفسه بخطوات إضافية. ومن دون حدّ لمعدّل الطلبات، يستطيع مستخدم واحد مسجّل الدخول أن يراكم فاتورة كبيرة. ومن دون سقف للمخرجات، قد يولّد طلب واحد رموزًا (tokens) أكثر بكثير مما تحتاجه ميزتك.

أبقِ اسم النموذج والحدود على الخادم أيضًا

إذا كان المتصفح يرسل اسم النموذج أو max_output_tokens، فيستطيع الزائر تغييرهما. حدّد هذه القيم على جانب الخادم، ولا تقبل من العميل إلا رسالة المستخدم.

إذا كان المفتاح قد نُشر بالفعل

إزالة المتغير من الشيفرة لا تكفي وحدها. فالإصدارات القديمة من البناء تبقى مخزّنة مؤقتًا في المتصفحات وشبكات CDN، وربما نُسخ المفتاح بالفعل. تعامل معه على أنه مخترَق:

  1. أنشئ مفتاحًا جديدًا في لوحة تحكم المزوّد وألغِ المفتاح القديم.
  2. ضع المفتاح الجديد في إعدادات البيئة على جانب الخادم فقط، باسم لا يحمل بادئة عامة.
  3. راجع صفحة الاستخدام لدى المزوّد بحثًا عن حركة لا تعرفها، واضبط حدًّا للإنفاق.
  4. أعد النشر، ثم كرّر البحث في البناء المذكور أعلاه للتأكد من أن الحزمة الجديدة نظيفة.

إذا كان المفتاح قد أُودع في git في أي وقت، فبدّله حتى لو حذفت الملف. فتاريخ git يحتفظ بالقيمة القديمة.

اكتشفه قبل أن يُنشر

من السهل التخلص من هذا الخطأ بالأتمتة. فأداة فحص الأسرار تكتشف المفاتيح المُودَعة في المستودع. لكنها لا تستطيع رؤية مفتاح يعيش في ملف .env متجاهَل، لذا تحتاج أيضًا إلى فحص يقرأ الشيفرة ويلاحظ تمرير متغير عام إلى عميل LLM.

هذا النمط الثاني إحدى قواعد أمان LLM والوكلاء في الفحوص الكاملة لـ vetkit: فهو ينبّه إلى قيمة قادمة من متغير بيئة على جانب العميل (مثل import.meta.env.VITE_* أو process.env.NEXT_PUBLIC_*) تصل إلى عميل LLM SDK في شيفرة المتصفح، ويشرح كيف تنقل الاستدعاء إلى جانب الخادم. ويغطي Security Quick Scan المجاني من vetkit الأسرار المُودَعة والاعتماديات المعروفة بثغراتها والوضع الأمني الأساسي. ولا يغني أيٌّ منهما عن نظرة سريعة على مخرجات البناء لديك، فهي أكثر دليل مباشر يمكنك الحصول عليه.

طلب الوصول المبكر

حدّثنا عن فريقك والمستودعات التي تريد تدقيقها. نُتيح الوصول لفرق على دفعات صغيرة، وسنرد عليك عبر البريد الإلكتروني.

نستخدم هذه المعلومات للرد على طلبك فقط. سياسة الخصوصية