بدء استخدام Sandbox2

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

1. اختيار طريقة Sandbox Executor

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

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

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

في مقتطف الرمز التالي، نحدّد مسار الملف الثنائي الذي سيتم وضعه في وضع الحماية والوسيطات التي يجب تمريرها إلى استدعاء نظام 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: Sandbox2 Forkserver – تحديد وقت وضع التنفيذ في وضع الحماية

توفّر هذه الطريقة مرونة في عدم استخدام وضع الحماية أثناء الإعداد، ثم اختيار وقت الدخول إلى وضع الحماية من خلال استدعاء ::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 هو البرنامج الخاضع لبيئة معزولة ويُرسل إشعارًا إلى المنفِّذ (crc4sandbox.cc) عندما يحين وقت الدخول إلى البيئة المعزولة.

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

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

سيرسل المنفّذ طلب إنشاء نسخة إلى الرمز الثنائي، والذي سيتم fork() (عبر ::sandbox2::ForkingClient::EnterForkLook()). وستكون العملية التي تم إنشاؤها حديثًا جاهزة ليتم وضعها في وضع الحماية باستخدام ::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 التلقائية.

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

بعد الحصول على قائمة استدعاءات النظام، يمكنك استخدام PolicyBuilder لتحديد السياسة.

تتضمّن PolicyBuilder العديد من الدوال المساعدة والميسّرة التي تتيح إجراء العديد من العمليات الشائعة (التي تم تضمين الكثير منها في DefaultPolicyBuilder()). القائمة التالية هي مقتطف صغير من الدوال المتاحة:

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

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

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

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

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

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

ينشئ المثال التالي sandbox2::PolicyBuilder مباشرةً لتوضيح تقييد وسيطات write وmprotect وحظر openat (التي قد يسمح بها DefaultPolicyBuilder()):

#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- تعديل الحدود

تمنع &quot;سياسة وضع الحماية&quot; تطبيق 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. تشغيل وضع الحماية

في الأقسام السابقة، أعددت بيئة وضع الحماية والسياسة والمنفِّذ والبرنامج المحمي. الخطوة التالية هي إنشاء الكائن 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) لإرسال أعداد صحيحة أو سلاسل أو مخازن مؤقتة للبايت أو مخازن بروتوكول مؤقتة أو واصفات ملفات.

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

باستخدام واجهة برمجة التطبيقات Inter-Process Communication (راجِع 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 واجهة برمجة تطبيقات comms سهلة الاستخدام. هذه طريقة بسيطة وسهلة لمشاركة الأعداد الصحيحة أو السلاسل أو مخازن البايت المؤقتة بين المنفِّذ وSandboxee. في ما يلي بعض مقتطفات الرموز التي يمكنك العثور عليها في مثال crc4.

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

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

Once the comms object is available, data can be sent to the Sandboxee using one of the Send* family of functions. يمكنك العثور على مثال على استخدام واجهة برمجة التطبيقات للاتصالات في مثال 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.

In the code snippet below, you can see the executor's side. يتم تشغيل وضع الحماية بشكل غير متزامن ويشارك البيانات عبر مخزن مؤقت مع 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++ باستخدام العمليات الفرعية. اطّلِع على الأمثلة للحصول على أفكار.

الخاتمة

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

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