بدء استخدام Sandbox2

في هذه الصفحة، ستتعرّف على كيفية إنشاء بيئة معزولة باستخدام Sandbox2. ستتعرّف على كيفية تحديد سياسة Sandbox، وبعض التعديلات المتقدّمة والشائعة. استخدِم المعلومات الواردة هنا كدليل، بالإضافة إلى الأمثلة ومستندات الرموز البرمجية في ملفات العناوين.

1. اختيار طريقة تنفيذ في وضع الحماية

يبدأ وضع الحماية باستخدام برنامج تنفيذي (راجِع Sandbox Executor)، وهو المسؤول عن تشغيل Sandboxee. يحتوي ملف العنوان executor.h على واجهة برمجة التطبيقات اللازمة لهذا الغرض. تتسم واجهة برمجة التطبيقات بالمرونة الشديدة وتتيح لك اختيار ما يناسب حالة الاستخدام. توضّح الأقسام التالية الأساليب الثلاثة المختلفة التي يمكنك الاختيار من بينها.

الطريقة 1: مستقلة – تنفيذ ملف ثنائي مع تفعيل وضع الحماية

هذه هي أبسط طريقة لاستخدام وضع الحماية، وهي الطريقة المقترَحة عندما تريد وضع برنامج ثنائي كامل في وضع الحماية بدون توفّر الرمز المصدر. وهي أيضًا الطريقة الأكثر أمانًا لاستخدام وضع الحماية، لأنّه لا يوجد تهيئة خارج وضع الحماية يمكن أن يكون لها تأثيرات سلبية.

في مقتطف الرمز أدناه، نحدّد مسار الملف الثنائي الذي سيتم وضعه في وضع الحماية، والوسيطات التي يجب تمريرها إلى استدعاء النظام execve. كما هو موضّح في ملف العنوان executor.h، لا نحدّد قيمة لـ envp، وبالتالي ننسخ البيئة من العملية الرئيسية. تذكَّر أنّ الوسيطة الأولى هي دائمًا اسم البرنامج المطلوب تنفيذه، وأنّ مقتطف الرمز لا يحدّد أي وسيطة أخرى.

من الأمثلة على طريقة التنفيذ هذه: static و tool.

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};  // args[0] will become the sandboxed
                                         // process' argv[0], typically the
                                         // path to the binary.
auto executor = absl::make_unique<sandbox2::Executor>(path, args);

الطريقة 2: Forkserver في Sandbox2 - تحديد وقت وضع التنفيذ في وضع الحماية

توفّر هذه الطريقة مرونة عدم استخدام وضع الحماية أثناء عملية التهيئة، ثم اختيار وقت تفعيل وضع الحماية من خلال استدعاء ::sandbox2::Client::SandboxMeHere(). ويجب أن تكون قادرًا على تحديد وقت بدء وضع الحماية في الرمز البرمجي، كما يجب أن يكون الرمز البرمجي أحادي السلسلة (يمكنك الاطّلاع على السبب في الأسئلة الشائعة).

في مقتطف الرمز البرمجي أدناه، نستخدم الرمز نفسه الموضّح في "الطريقة 1" أعلاه. ومع ذلك، للسماح بتنفيذ البرنامج بطريقة غير محصورة في بيئة الاختبار المعزولة أثناء عملية التهيئة، نستدعي الدالة set_enable_sandbox_before_exec(false).

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
executor->set_enable_sandbox_before_exec(false);

بما أنّ المنفّذ لديه الآن بيئة اختبار معطّلة إلى أن يتم إعلامه من خلال Sandboxee، علينا إنشاء مثيل ::sandbox2::Client، وإعداد عملية التواصل بين المنفّذ وSandboxee، ثم إعلام المنفّذ بأنّ عملية الإعداد قد انتهت وأنّنا نريد بدء استخدام بيئة الاختبار الآن من خلال استدعاء sandbox2_client.SandboxMeHere().

// main() of sandboxee
int main(int argc, char** argv) {
  gflags::ParseCommandLineFlags(&argc, &argv, false);

  // Set-up the sandbox2::Client object, using a file descriptor (1023).
  sandbox2::Comms comms(sandbox2::Comms::kSandbox2ClientCommsFD);
  sandbox2::Client sandbox2_client(&comms);
  // Enable sandboxing from here.
  sandbox2_client.SandboxMeHere();
  

أحد الأمثلة على طريقة التنفيذ هذه هو crc4، حيث يمثّل crc4bin.cc Sandboxee ويُعلم المنفّذ (crc4sandbox.cc) بموعد الدخول إلى وضع الحماية.

الطريقة 3: Forkserver مخصّص - إعداد ملف ثنائي، والانتظار لتلقّي طلبات fork، وتوفير بيئة الاختبار المعزولة بنفسك

يتيح لك هذا الوضع بدء تشغيل ملف ثنائي وإعداده لتشغيله في بيئة معزولة، كما يتيح لك إتاحته للمنفّذ في لحظة معيّنة من دورة حياة الملف الثنائي.

سيرسل برنامج التنفيذ طلب إنشاء نسخة إلى الرمز الثنائي، والذي سيتم fork() (عبر ::sandbox2::ForkingClient::WaitAndFork()). وستكون العملية التي تم إنشاؤها حديثًا جاهزة لوضعها في بيئة الاختبار المعزولة باستخدام ::sandbox2::Client::SandboxMeHere().

#include "sandboxed_api/sandbox2/executor.h"

// Start the custom ForkServer
std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto fork_executor = absl::make_unique<sandbox2::Executor>(path, args);
fork_executor->StartForkServer();

// Initialize Executor with Comms channel to the ForkServer
auto executor = absl::make_unique<sandbox2::Executor>(
    fork_executor->ipc()->GetComms());

يُرجى العِلم أنّ هذا الوضع معقّد جدًا ولا ينطبق إلا في حالات محدودة، مثل عندما تكون لديك متطلبات صارمة بشأن الذاكرة. ستستفيد من COW ولكن سيكون هناك عيب يتمثل في عدم توفّر ASLR حقيقي. من الأمثلة الأخرى على الاستخدام الشائع أن يكون لدى Sandboxee عملية إعداد طويلة ومكثفة لوحدة المعالجة المركزية يمكن تشغيلها قبل معالجة البيانات غير الموثوق بها.

للاطّلاع على مثال على طريقة التنفيذ هذه، راجِع custom_fork.

2. إنشاء سياسة "وضع الحماية"

بعد توفّر برنامج تنفيذي، من المحتمل أن تحتاج إلى تحديد سياسة Sandbox لبرنامج Sandboxee. بخلاف ذلك، لا تتم حماية Sandboxee إلا من خلال سياسة Syscall التلقائية ‎(Default Syscall Policy).

تهدف "سياسة وضع الحماية" إلى حصر طلبات النظام (syscalls) والوسيطات التي يمكن أن يقدّمها Sandboxee، بالإضافة إلى الملفات التي يمكنه الوصول إليها. يجب أن يكون لديك فهم تفصيلي لطلبات النظام المطلوبة من الرمز الذي تخطّط لوضعه في بيئة معزولة. تتمثل إحدى طرق مراقبة طلبات النظام في تشغيل الرمز باستخدام أداة سطر الأوامر strace في Linux.

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

  • إدراج أي استدعاء نظام في القائمة المسموح بها لبدء العملية:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • إضافة أي استدعاءات نظام مفتوحة/قراءة/كتابة* إلى القائمة المسموح بها:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • إضافة أي استدعاءات نظام مرتبطة بالخروج أو الوصول أو الحالة إلى القائمة المسموح بها:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • إضافة أي طلبات نظام ذات صلة بالنوم/الوقت إلى القائمة المسموح بها:
    • AllowTime();
    • AllowSleep();

تسمح وظائف التيسير هذه بإدراج أي استدعاء نظام ذي صلة في القائمة المسموح بها. ويتميّز ذلك بميزة إمكانية استخدام السياسة نفسها في بنى مختلفة لا تتوفّر فيها بعض استدعاءات النظام (مثل ARM64 التي لا تتضمّن استدعاء نظام OPEN)، ولكن مع خطر أمني بسيط يتمثّل في تفعيل عدد أكبر من استدعاءات النظام التي قد تكون ضرورية. على سبيل المثال، يتيح AllowOpen() لـ Sandboxee استدعاء أي نظام syscall مفتوح ذي صلة. إذا أردت إدراج استدعاء نظام واحد فقط في القائمة المسموح بها، يمكنك استخدام AllowSyscall();. أما إذا أردت إدراج استدعاءات نظام متعددة في القائمة المسموح بها في آنٍ واحد، فيمكنك استخدام AllowSyscalls().

حتى الآن، تتحقّق السياسة من معرّف استدعاء النظام فقط. إذا كنت بحاجة إلى تعزيز السياسة بشكل أكبر وأردت تحديد سياسة لا تسمح إلا باستدعاء نظامي مع وسيطات معيّنة، عليك استخدام AddPolicyOnSyscall() أو AddPolicyOnSyscalls(). لا تقبل هذه الدوال معرّف طلب النظام كوسيط فحسب، بل تقبل أيضًا فلتر seccomp-bpf أوليًا باستخدام وحدات ماكرو مساعدة bpf من نواة Linux. يمكنك الاطّلاع على مستندات نظام التشغيل للحصول على مزيد من المعلومات حول BPF. إذا كنت تكتب رمز BPF متكررًا تعتقد أنّه يجب أن يتضمّن برنامج تضمين قابل للاستخدام، يمكنك تقديم طلب للحصول على ميزة.

بالإضافة إلى الدوال ذات الصلة باستدعاءات النظام، يوفّر PolicyBuilder أيضًا عددًا من الدوال ذات الصلة بنظام الملفات، مثل AddFile() أو AddDirectory()، لربط ملف أو دليل في وضع الحماية. يمكن استخدام أداة AddTmpfs() المساعدة لإضافة مساحة تخزين مؤقتة للملفات داخل البيئة الافتراضية.

إحدى الدوال المفيدة بشكل خاص هي AddLibrariesForBinary() التي تضيف المكتبات وبرنامج الربط المطلوبَين بواسطة ملف ثنائي.

لا يزال تحديد استدعاءات النظام التي يجب إضافتها إلى القائمة المسموح بها يتطلّب بعض العمل اليدوي. أنشئ سياسة تتضمّن استدعاءات النظام التي تعرف أنّ برنامجك الثنائي يحتاجها، ونفِّذها باستخدام عبء عمل شائع. في حال حدوث انتهاك، أضِف syscall إلى القائمة المسموح بها وكرِّر العملية. إذا واجهت انتهاكًا تعتقد أنّه قد يكون محفوفًا بالمخاطر إذا أضفته إلى القائمة المسموح بها وكان البرنامج يتعامل مع الأخطاء بشكل سليم، يمكنك محاولة إرجاع خطأ بدلاً من ذلك باستخدام BlockSyscallWithErrno().

#include "sandboxed_api/sandbox2/policy.h"
#include "sandboxed_api/sandbox2/policybuilder.h"
#include "sandboxed_api/sandbox2/util/bpf_helper.h"

std::unique_ptr<sandbox2::Policy> CreatePolicy() {
  return sandbox2::PolicyBuilder()
    .AllowSyscall(__NR_read)  // See also AllowRead()
    .AllowTime()              // Allow time, gettimeofday and clock_gettime
    .AddPolicyOnSyscall(__NR_write, {
        ARG(0),        // fd is the first argument of write (argument #0)
        JEQ(1, ALLOW), // allow write only on fd 1
        KILL,          // kill if not fd 1
    })
    .AddPolicyOnSyscall(__NR_mprotect, {
        ARG_32(2), // prot is a 32-bit wide argument, so it's OK to use *_32
                   // macro here
        JNE32(PROT_READ | PROT_WRITE, KILL), // prot must be the RW, otherwise
                                             // kill the process
        ARG(1), // len is a 64-bit argument
        JNE(0x1000, KILL),  // Allow single page syscalls only, otherwise kill
                            // the process
        ALLOW,              // Allow for the syscall to proceed, if prot and
                            // size match
    })
    // Allow the openat() syscall but always return "not found".
    .BlockSyscallWithErrno(__NR_openat, ENOENT)
    .BuildOrDie();
}

3- تعديل الحدود

تمنع "سياسة وضع الحماية" تطبيق Sandboxee من استدعاء بعض استدعاءات النظام، وبالتالي تقلّل من الأجزاء المُعرضة للهجوم. ومع ذلك، قد يظل بإمكان المهاجم التسبّب في آثار غير مرغوب فيها من خلال تشغيل عملية إلى أجل غير مسمى أو استنفاد ذاكرة الوصول العشوائي (RAM) والموارد الأخرى.

لمواجهة هذا التهديد، يتم تشغيل Sandboxee ضمن حدود تنفيذ صارمة بشكل تلقائي. إذا تسبّبت هذه الحدود التلقائية في حدوث مشاكل في التنفيذ السليم لبرنامجك، يمكنك تعديلها باستخدام فئة sandbox2::Limits من خلال استدعاء limits() على عنصر المنفّذ.

يعرض مقتطف الرمز البرمجي أدناه بعض الأمثلة على تعديلات الحدّ الأقصى. جميع الخيارات المتاحة موضّحة في ملف العنوان limits.h.

// Restrict the address space size of the sandboxee to 4 GiB.
executor->limits()->set_rlimit_as(4ULL << 30);
// Kill sandboxee with SIGXFSZ if it writes more than 1 GiB to the filesystem.
executor->limits()->set_rlimit_fsize(1ULL << 30);
// Number of file descriptors which can be used by the sandboxee.
executor->limits()->set_rlimit_nofile(1ULL << 10);
// The sandboxee is not allowed to create core files.
executor->limits()->set_rlimit_core(0);
// Maximum 300s of real CPU time.
executor->limits()->set_rlimit_cpu(300);
// Maximum 120s of wall time.
executor->limits()->set_walltime_limit(absl::Seconds(120));

للاطّلاع على مثال حول استخدام الفئة sandbox2::Limits، راجِع المثال للأداة.

4. تشغيل "وضع الحماية"

في الأقسام السابقة، أعددت البيئة المحصورة في وضع الحماية والسياسة وبرنامج التنفيذ وSandboxee. الخطوة التالية هي إنشاء الكائن Sandbox2 وتشغيله.

التشغيل بشكل متزامن

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

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
sandbox2::Result result = s2.Run();  // Synchronous
LOG(INFO) << "Result of sandbox execution: " << result.ToString();

التنفيذ بشكل غير متزامن

يمكنك أيضًا تشغيل وضع الحماية بشكل غير متزامن، وبالتالي عدم الحظر إلى أن تتوفر نتيجة. ويفيد ذلك مثلاً عند التواصل مع Sandboxee. يوضّح مقتطف الرمز البرمجي أدناه حالة الاستخدام هذه، وللاطّلاع على أمثلة أكثر تفصيلاً، يُرجى الرجوع إلى crc4 وtool.

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
if (s2.RunAsync()) {
  // Communicate with sandboxee, use s2.Kill() to kill it if needed
  // ...
}
Sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

5- التواصل مع Sandboxee

يمكن للمنفّذ تلقائيًا التواصل مع Sandboxee من خلال واصفات الملفات. قد يكون هذا كل ما تحتاج إليه، مثلاً إذا كنت تريد مشاركة ملف مع Sandboxee أو قراءة الناتج العادي لـ Sandboxee.

ومع ذلك، من المرجّح أنّك بحاجة إلى منطق اتصال أكثر تعقيدًا بين المنفِّذ وSandboxee. يمكن استخدام واجهة برمجة التطبيقات الخاصة بالاتصالات (راجِع ملف العنوان comms.h) لإرسال أعداد صحيحة أو سلاسل أو مخازن مؤقتة للبايتات أو بروتوكولات protobuf أو واصفات الملفات.

مشاركة واصفات الملفات

باستخدام واجهة برمجة التطبيقات Inter-Process Communication API (راجِع ipc.h)، يمكنك استخدام MapFd() أو ReceiveFd():

  • استخدِم MapFd() لربط أوصاف الملفات من المنفِّذ بـ Sandboxee. يمكن استخدام هذا الخيار لمشاركة ملف تم فتحه من أداة التنفيذ لاستخدامه في Sandboxee. يمكن الاطّلاع على مثال على الاستخدام في static.

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • استخدِمReceiveFd() لإنشاء نقطة نهاية socketpair. يمكن استخدام هذا الخيار لقراءة المخرجات القياسية أو الأخطاء القياسية في Sandboxee. يمكن الاطّلاع على مثال على الاستخدام في الأداة.

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

استخدام واجهة برمجة التطبيقات الخاصة بالاتصالات

توفّر Sandbox2 واجهة برمجة تطبيقات للاتصالات سهلة الاستخدام. هذه طريقة بسيطة وسهلة لمشاركة الأعداد الصحيحة أو السلاسل أو مخازن مؤقتة للبايتات بين المنفِّذ وSandboxee. في ما يلي بعض مقتطفات الرموز التي يمكنك العثور عليها في مثال crc4.

للبدء باستخدام واجهة برمجة التطبيقات الخاصة بالاتصالات، عليك أولاً الحصول على عنصر الاتصالات من عنصر Sandbox2:

sandbox2::Comms* comms = s2.comms();

بعد توفّر كائن الاتصالات، يمكن إرسال البيانات إلى Sandboxee باستخدام إحدى دوال Send* المتشابهة. يمكنك العثور على مثال على استخدام واجهة برمجة التطبيقات comms API في المثال crc4. يعرض مقتطف الرمز أدناه جزءًا من هذا المثال. يرسل المنفّذ unsigned char buf[size] مع SendBytes(buf, size):

if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
  /* handle error */
}

لتلقّي البيانات من Sandboxee، استخدِم إحدى وظائف Recv*. مقتطف الرمز البرمجي أدناه هو جزء من مثال crc4. يتلقّى المنفِّذ المجموع الاختباري في عدد صحيح غير موقّع يبلغ 32 بت: uint32_t crc4;

if (!(comms->RecvUint32(&crc4))) {
  /* handle error */
}

مشاركة البيانات مع المخازن المؤقتة

تتمثّل إحدى وظائف مشاركة البيانات الأخرى في استخدام واجهة برمجة التطبيقات buffer ‎ لمشاركة كميات كبيرة من البيانات وتجنُّب النسخ المكلفة التي يتم إرسالها بين المنفّذ وSandboxee.

ينشئ المنفِّذ مخزنًا مؤقتًا، إما حسب الحجم والبيانات التي سيتم تمريرها، أو مباشرةً من واصف ملف، ويمرّره إلى Sandboxee باستخدام comms->SendFD() في المنفِّذ وcomms->RecvFD() في Sandboxee.

في مقتطف الرمز البرمجي أدناه، يمكنك الاطّلاع على جهة المنفّذ. تعمل البيئة التجريبية بشكل غير متزامن وتشارك البيانات عبر مخزن مؤقت مع Sandboxee:

// start the sandbox asynchronously
s2.RunAsync();

// instantiate the comms object
sandbox2::Comms* comms = s2.comms();

// random buffer data we want to send
constexpr unsigned char buffer_data[] = /* random data */;
constexpr unsigned int buffer_dataLen = 34;

// create sandbox2 buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::CreateWithSize(1ULL << 20 /* 1Mib */);
std::unique_ptr<sandbox2::Buffer> buffer_ptr = std::move(buffer).value();

// point to the sandbox2 buffer and fill with data
uint8_t* buf = buffer_ptr>data();
memcpy(buf, buffer_data, buffer_data_len);

// send the data to the sandboxee
comms>SendFd(buffer_ptr>fd());

على جانب Sandboxee، عليك أيضًا إنشاء عنصر مؤقت وقراءة البيانات من واصف الملف الذي أرسله المنفّذ:

// establish the communication with the executor
int fd;
comms.RecvFD(&fd);

// create the buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::createFromFd(fd);

// get the data
auto buffer_ptr = std::move(buffer).value();
uint8_t* buf = buffer_ptr>data();

/* work with the buf object */

6. الخروج من وضع الحماية

استنادًا إلى طريقة تشغيل وضع الحماية (راجِع هذه الخطوة)، عليك تعديل طريقة إنهاء وضع الحماية، وبالتالي أيضًا Sandboxee.

الخروج من وضع الحماية الذي يتم تنفيذه بشكل متزامن

إذا كان وضع الحماية يعمل بشكل متزامن، لن تعرض الدالة Run أي قيمة إلا بعد انتهاء Sandboxee. وبالتالي، لا يلزم اتّخاذ أي خطوة إضافية لإنهاء الاشتراك. يوضّح مقتطف الرمز أدناه هذا السيناريو:

Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();

الخروج من وضع الحماية الذي يتم تشغيله بشكل غير متزامن

إذا كانت البيئة التجريبية تعمل بشكل غير متزامن، سيتوفّر خياران لإيقافها. أولاً، يمكنك الانتظار إلى أن تكتمل عملية Sandboxee وتتلقّى حالة التنفيذ النهائية:

sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

يمكنك بدلاً من ذلك إيقاف Sandboxee في أي وقت، ولكن لا يزال يُنصح باستدعاء AwaitResult() لأنّ Sandboxee قد يتم إيقافه لسبب آخر في الوقت الحالي:

s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

7. اختبار

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

يتم اختبار البرنامج المحمي في بيئة معزولة بالطريقة نفسها التي يتم تشغيله بها في مرحلة الإنتاج، مع الوسيطات وملفات الإدخال التي تتم معالجتها عادةً.

يمكن أن تكون هذه الاختبارات بسيطة مثل اختبار shell أو اختبارات C++ باستخدام العمليات الفرعية. يمكنك الاطّلاع على الأمثلة للحصول على أفكار.

الخاتمة

نشكرك على قراءة هذا الدليل حتى النهاية، ونأمل أن يكون قد نال إعجابك وأصبح بإمكانك الآن إنشاء بيئات الاختبار المعزولة الخاصة بك للمساعدة في الحفاظ على أمان المستخدمين.

إنشاء بيئات الاختبار المعزولة والسياسات مهمة صعبة وعُرضة للأخطاء الطفيفة. للحفاظ على أمانك، ننصحك بأن يراجع أحد خبراء الأمان سياستك ورمزك.