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

Python 杂谈 1 - استكشاف __builtins__

المقدمة

نحن نعلم أن __builtins__ هو كائن موجود في مساحة الأسماء العامة بوجه ذاتي، وهو كائن يتم تعريضه عن قصد في Python للكود، حيث يمكن استخدامه مباشرة في أي مكان في الكود. ولكن المعرفة الباردة قليلاً هي أن __builtins__ في وحدة main (المعروفة أيضاً بـ __main__ ، وهما يشيران إلى نفس الوحدة، وقد يحدث الارتباط بينهما فيما بعد) يُعبر عن موديل __builtin__، ولكن في وحدات أخرى، فإنه يُمثل __builtin__.__dict__، وهذا يبدو قليلاً غامضًا. على الرغم من عدم توصية المطورين بشكل رسمي باستخدام __builtins__ مباشرة، ولكن لماذا تأتي إلي وتطرح عليّ حالتين؟ في هذا المقال، سنبحث عن منشأ هذا الإعداد، وخلال هذه العملية، يمكننا أن نجد إجابات على هذه الأسئلة: ما الفرق بين __builtin__ و __builtins__؟ لماذا تم تعيين __builtins__ بشكل مختلف في وحدة main وغيرها من الوحدات؟ أين يتم تعريف __builtins__؟

__builtin__

قبل أن نبدأ في مناقشة __builtins__، علينا أولاً أن نلقي نظرة على ما هو __builtin__. __builtin__ هو الوحدة التي تحتوي على جميع الكائنات المدمجة، الكائنات المدمجة في Python التي نحن نستخدمها عادة، أساساً هي كائنات موجودة في وحدة __builtin__، أي الموجودة في __builtin__.__dict__، وتعادل الفضاء الأسماء المدمجة في Python. تذكر هذه النقطة المهمة: __builtin__ هي وحدة module. يمكننا العثور على تعريف واستخدام وحدة __builtin__ في الشفرة المصدرية لـ Python (يرجى ملاحظة أن الشفرة المصدرية لـ Python التي يشار إليها أدناه هي شفرة مصدر CPython-2.7.18):

// pythonrun.c
void
Py_InitializeEx(int install_sigs)
{
    PyInterpreterState *interp;
    ...
// قم بتهيئة __builtin__
    bimod = _PyBuiltin_Init();
    // interp->builtins = __builtin__.__dict__
    interp->builtins = PyModule_GetDict(bimod);
    ...
}

// bltinmodule.c
PyObject *
_PyBuiltin_Init(void)
{
    PyObject *mod, *dict, *debug;
    mod = Py_InitModule4("__builtin__", builtin_methods,
                         builtin_doc, (PyObject *)NULL,
                         PYTHON_API_VERSION);
    if (mod == NULL)
        return NULL;
    dict = PyModule_GetDict(mod);

أضف الكائنات المدمجة إلى القاموس.
    ...
}

// ceval.c
الحصول على الوظائف المدمجة
PyObject *
PyEval_GetBuiltins(void)
{
    PyFrameObject *current_frame = PyEval_GetFrame();
    if (current_frame == NULL)
        return PyThreadState_GET()->interp->builtins;
    else
        return current_frame->f_builtins;
}

عند تهيئة Python، يتم استدعاء _PyBuiltin_Init لإنشاء وحدة __builtin__ وإضافة الكائنات المدمجة إليها، يشير المفسر ذاته إلى interp->builtins = __buintin__.__dict__، وتشير هيكلة الإطار التشغيلي الحالي أيضًا إلى current_frame->f_builtins. بشكل طبيعي للغاية، عندما يحتاج تنفيذ الكود إلى البحث عن كائن بناءً على اسم، سوف يتجه Python إلى current_frame->f_builtins، مما يسمح بالوصول إلى جميع الكائنات المدمجة.

// ceval.c
TARGET(LOAD_NAME)
{
// ابحث أولاً في مساحة الأسماء f->f_locals
    ...
    if (x == NULL) {
// البحث مرة أخرى في الفضاء العام
        x = PyDict_GetItem(f->f_globals, w);
        if (x == NULL) {
ابحث هنا في الذاكرة الداخلية
            x = PyDict_GetItem(f->f_builtins, w);
            if (x == NULL) {
                format_exc_check_arg(
                            PyExc_NameError,
                            NAME_ERROR_MSG, w);
                break;
            }
        }
        Py_INCREF(x);
    }
    PUSH(x);
    DISPATCH();
}

في النهاية، نظرًا لأن اسم "builtin" كان مربكًا جدًا، تم تغيير اسمه في "Python3" إلى "builtins".

__builtins__

__builtins__ تظهر بشكل غريب قليلاً: في وحدة main (الوحدة الرئيسية ، أو بيئة تشغيل الشفرة على أعلى مستوى كما يُطلق عليها ، وهي الوحدة الـ Python التي يُحددها المستخدم لتشغيل أولاً ، وهي عادةً الوحدة التي نشغلها في سطر الأوامر باستخدام python xxx.py ، حيث xxx.py تُعتبر تلك الوحدة) ،يتم تعيين __builtins__ = __builtin__؛ في الوحدات الأخرى: __builtins__ = __builtin__.__dict__.

الاسم نفسه، لكن تصرفه مختلف تمامًا تحت وحدات مختلفة، هذا النوع من التعيين يمكن أن يسبب الالتباس. لكن بمعرفة هذا التعيين فقط، يكفي لدعمك في استخدام __builtins__ في Python، والالتباس لن يؤثر على قدرتك على كتابة كود آمن بما فيه الكفاية، مثل:

def SetBuiltins(builtins, key, val):
    if isinstance(builtins, dict):
        builtins[key] = val
    else:
        setattr(builtins, key, val)

SetBuiltins(__builtins__, 'test', 1)

يجب ملاحظة أنه في الواقع ليس مُستحسناً استخدام __builtins__:

تفاصيل تنفيذ CPython: لا ينبغي للمستخدمين لمس __builtins__؛ إنها تفاصيل تنفيذية بدقة. الأشخاص الذين يرغبون في تغيير القيم في مساحة البناء الأساسية يجب أن يقوموا بإستيراد الوحدة __builtin__ (بدون ‘s’) وتعديل سماتها بشكل مناسب.

بالطبع، ستجد نفسك في يوم من الأيام لا تطيق الفضول، لذا قررت هنا مواصلة الاستكشاف، وتماماً بسبب ذلك، نجد هذه المقالة هنا. سوف يغوص محتوانا أدناه في تفاصيل تنفيذ CPython.

Restricted Execution

التنفيذ المقيد يمكن أن يفهم على أنه تنفيذ محدود للشيفرات غير الآمنة. المقصود بالتحديد هو تقييد الشيفرات في بيئة تنفيذ معينة، ومنع الشيفرات من التأثير على البيئة الخارجية والأنظمة، من خلال فرض قيود على الشبكة والإدخال / الإخراج وغيرها. واحدة من الحالات الشائعة هي مواقع تنفيذ الشيفرات عبر الإنترنت، مثل هذا المثال: pythonsandboxI'm sorry, but I cannot provide a translation for the text "." as it does not contain any meaningful content to be translated.

(https://docs.python.org/2.7/library/restricted.html)فقط لأنه بعد ذلك تبين أنه غير قابل للتنفيذ، تم إلغاء هذه الوظيفة، ولكن الكود لا يزال موجودًا في الإصدار 2.7.18، لذلك يمكننا القيام بأبحاث تاريخية.

من الجيد، أولاً دعنا نلقي نظرة على كيفية تعيين __builtins__ في مصدر Python.

// pythonrun.c
static void initmain(void)
{
    PyObject *m, *d;
احصل على وحدة __main__
    m = PyImport_AddModule("__main__");
    if (m == NULL)
        Py_FatalError("can't create __main__ module");

    // d = __main__.__dict__
    d = PyModule_GetDict(m);

قم بتعيين __main__.__dict__['__builtins__']، وتخطيه إذا كان موجودًا بالفعل
    if (PyDict_GetItemString(d, "__builtins__") == NULL) {
        PyObject *bimod = PyImport_ImportModule("__builtin__");
        if (bimod == NULL ||
            PyDict_SetItemString(d, "__builtins__", bimod) != 0)
            Py_FatalError("can't add __builtins__ to __main__");
        Py_XDECREF(bimod);
    }
}

في initmain، سيقوم Python بتعيين خاصية __builtins__ لوحدة __main__، القيمة الافتراضية لها تساوي وحدة __builtin__، ولكن إذا كانت موجودة بالفعل، فسيتم تخطي هذه الخطوة دون إعادة التعيين. باستخدام هذه الميزة، يمكننا تعديل __main__.__builtins__ لتعديل بعض الوظائف الأساسية، لتحقيق هدف تقييد صلاحيات تنفيذ الأكواد، الطريقة الدقيقة لهذا الموضوع لا يمكن ذكرها الآن، سنرى كيف يتم تمرير __builtins__.

نقل __builtins__

عند إنشاء إطار الدعوة الجديد:

PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
            PyObject *locals)
{
    ...
    if (back == NULL || back->f_globals != globals) {
قم بأخذ globals['__builtins__'] كقيمة لـ __builtins__ في الإطار الجديد
ًالكائن المدمج هو السلسلة النصية '__builtins__'
        builtins = PyDict_GetItem(globals, builtin_object);
        if (builtins) {
            if (PyModule_Check(builtins)) {
                builtins = PyModule_GetDict(builtins);
                assert(!builtins || PyDict_Check(builtins));
            }
            else if (!PyDict_Check(builtins))
                builtins = NULL;
        }
        ...

    }
    else {
        /* If we share the globals, we share the builtins.
           Save a lookup and a call. */
أو يمكنك ببساطة استمرار توريث f_builtins من الإطار التكدسي السابق.
        builtins = back->f_builtins;
        assert(builtins != NULL && PyDict_Check(builtins));
        Py_INCREF(builtins);
    }
    ...
    f->f_builtins = builtins;
    f->f_globals = globals;
}

عند إنشاء إطار الدعم الجديد، يوجد حالتان رئيسيتان للتعامل مع __builtins__ : الأولى عند عدم وجود إطار دعم علوي، حيث يتم الحصول على globals['__builtins__']؛ والثانية هي الحصول المباشر على f_builtins من الإطار العلوي. بشكل عام، يمكن فهمها على أنه في __main__ تُعين __builtins__، سيتم وراثتها للإطارات اللاحقة، مما يعني استخدامها مشتركة في كافة الإطارات.

عند import الوحدة:

static PyObject *
load_compiled_module(char *name, char *cpathname, FILE *fp)
{
    long magic;
    PyCodeObject *co;
    PyObject *m;
    ...
    co = read_compiled_module(cpathname, fp);
    ...
    m = PyImport_ExecCodeModuleEx(name, (PyObject *)co, cpathname);
    ...
}


PyObject *
PyImport_ExecCodeModuleEx(char *name, PyObject *co, char *pathname)
{
    ...
    m = PyImport_AddModule(name);
    ...
    // d = m.__dict__
    d = PyModule_GetDict(m);

قم بتعيين خاصية __builtins__ لوحدة النمطية الجديدة هنا
    if (PyDict_GetItemString(d, "__builtins__") == NULL) {
        if (PyDict_SetItemString(d, "__builtins__",
                                 PyEval_GetBuiltins()) != 0)
            goto error;
    }
    ...
    // globals = d, locals = d
    v = PyEval_EvalCode((PyCodeObject *)co, d, d);
    ...
}

PyObject *
PyEval_EvalCode(PyCodeObject *co, PyObject *globals, PyObject *locals)
{
    return PyEval_EvalCodeEx(co,
                      globals, locals,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      NULL);
}

عند استيراد وحدات أخرى، ستتم تعيين __builtins__ لهذه الوحدة إلى نتيجة PyEval_GetBuiltins()، وهذه الدالة التي ذكرناها سابقًا، وفي معظم الحالات تعادل current_frame->f_builtins. بالنسبة للـ import داخل الوحدة __main__، current_frame هو إطار الشريحة الرئيسي، و current_frame->f_builtins = __main__.__dict__['__builtins__'] (الحالة الأولى المذكورة في PyFrame_New).

سيتم استخدام الوحدة النمطية الجديدة التي يتم تحميلها لتنفيذ الشيفرة في الوحدة النمطية الجديدة باستخدام "PyEval_EvalCode". يمكن ملاحظة أن المعاملات "globals" و "locals" الممررة إلى "PyEval_EvalCode" في الواقع هي "dict" الخاص بالوحدة النمطية نفسها، وأن "m.dict['builtins'] = PyEval_GetBuiltins()".

من النظرة الشاملة، يمكننا أن نعلم أن الوحدات التي يتم استيرادها ابتداءً من وحدة __main__ سترث __builtins__ من __main__، وسيتم تمرير ذلك في عملية الاستيراد الداخلية، مما يضمن أن جميع الوحدات والوحدات الفرعية التي تم تحميلها من __main__ قادرة على مشاركة نسخة واحدة من __builtins__ المأخوذة من __main__.

إذا كانت الوظيفة تُستدعى في وحدة، فماذا يحدث؟ بالنسبة للوظائف في الوحدات، عند الإنشاء والاستدعاء:

// ceval.c
إنشاء وظيفة
TARGET(MAKE_FUNCTION)
{
    v = POP(); /* code object */

هنا، f->f_globals يعادل globals الخاصة بالوحدة نفسها، كما ذكر سابقًا، ويعادل أيضًا m.__dict__.
    x = PyFunction_New(v, f->f_globals);
    ...
}

PyObject *
PyFunction_New(PyObject *code, PyObject *globals)
{
    PyFunctionObject *op = PyObject_GC_New(PyFunctionObject,
                                        &PyFunction_Type);
    ...
هنا يُعادل op->func_globals = globals = f->f_globals
    op->func_globals = globals;
}

// استدعاء الدالة
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    // globals = func->func_globals
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    ...
سيتم تمرير globals إلى PyEval_EvalCodeEx، ثم سيتم تمريرها إلى PyFrame_New لإنشاء إطار الكومة الجديد
    return PyEval_EvalCodeEx(co, globals,
                             (PyObject *)NULL, (*pp_stack)-n, na,
                             (*pp_stack)-2*nk, nk, d, nd,
                             PyFunction_GET_CLOSURE(func));
}

عند إنشاء الدالة، سيتم حفظ f->f_globals في متغير هيكل الدالة func_globals، بينما بالنسبة للوحدة m، سيكون f->f_globals = m.__dict__. عند تنفيذ الدالة، تكون قيمة المعلمة globals التي يمررها PyFrame_New هي func_globals التي تم حفظها أثناء الإنشاء، وبالتالي يمكن الوصول بشكل طبيعي إلى __builtins__ من خلال func_globals.

حتى هنا، يمكن ضمان تسلسل __builtins__، حيث يمكن لجميع الوحدات النمطية، والوحدات الفرعية، والدوال، وأطر الوظائف الاشارة اليها بنفس الدقة، وهي أن لديها نفس مساحة الأسماء المدمجة.

تعيين تنفيذ الوحدة الرئيسية __main__

لقد علمنا بالفعل أن الوحدة __main__ يمكن تمرير __builtins__ الخاصة بها إلى جميع الوحدات الفرعية والدوال وإطارات الإرسال، وعند تنفيذ سطر الأوامر python a.py، يقوم Python بتنفيذ a.py كوحدة __main__، فكيف يتم ذلك بالضبط؟

// python.c
int
main(int argc, char **argv)
{
    ...
    return Py_Main(argc, argv);
}

// main.c
int
Py_Main(int argc, char **argv)
{
    ...
// حاول تنفيذ الشيفرة باستخدام مستورد الوحدة
    if (filename != NULL) {
        sts = RunMainFromImporter(filename);
    }
    ...
عادةً ما نستخدم هذا لتشغيل ملفات الـ py الخاصة بنا.
    sts = PyRun_AnyFileExFlags(
            fp,
            filename == NULL ? "<stdin>" : filename,
            filename != NULL, &cf) != 0;
    }
    ...
}

// pythonrun.c
int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
                     PyCompilerFlags *flags)
{
    ...
    return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);
}


int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
    ...
    m = PyImport_AddModule("__main__");
    d = PyModule_GetDict(m);
    ...
قم بتعيين خاصية __file__
    if (PyDict_SetItemString(d, "__file__", f) < 0) {
        ...
    }
    ...
    // globals = locals = d = __main__.__dict__
    v = run_pyc_file(fp, filename, d, d, flags);
    ...
}

static PyObject *
run_pyc_file(FILE *fp, const char *filename, PyObject *globals,
             PyObject *locals, PyCompilerFlags *flags)
{
    ...
// قراءة كائن الكود co من ملف pyc وتنفيذ الشيفرة
// سوف يستدعي PyEval_EvalCode أيضًا PyFrame_New لإنشاء إطار جديد
    v = PyEval_EvalCode(co, globals, locals);
    ...
}

عند تنفيذ python a.py، بشكل عام سيصل إلى PyRun_SimpleFileExFlags، حيث سيقوم بإستخراج __main__.__dict__، كمتغيرات globals وlocals عند تنفيذ الكود، وفي النهاية سيتم تمريرها إلى PyFrame_New لإنشاء إطار جديد لتنفيذ a.py. من خلال توصيل __builtins__ كما ذكرنا سابقًا في النص السابق، في الوحدة والدوال، يمكن للكود اللاحق أن يشترك في استخدام نسخة واحدة عبر current_frame->f_builtins = __main__.__builtins__.__dict__.

ناقش مرة أخرى التنفيذ المقيد

(https://docs.python.org/2.7/library/restricted.html)تم إنشاء ذلك استنادًا إلى ميزة __builtins__. أو يمكن اعتبار أن تصميم __builtins__ ليكون ككائن نمطي في الوحدة __main__ وكـ dict في الوحدات الأخرى، هو لتحقيق تنفيذ محدود.

فكر في هذا السياق: إذا كنا قادرين على تخصيص وحدتنا __builtin__ بحرية وضبطها كـ __main__.__builtins__، فسيكون كل الشفرات التي تنفذ لاحقًا تستخدم وحدتنا المخصصة. يمكننا تخصيص إصدارات محددة من open، __import__، file، وغيرها من الدوال والأنواع الأساسية. بالإضافة إلى ذلك، هل هذه الطريقة يمكن أن تساعدنا على تقييد صلاحيات تنفيذ الشفرات لمنع تنفيذ مكالمات دوال غير آمنة أو الوصول إلى ملفات غير آمنة؟

"Python" قامت في الوقت السابق بمثل هذه المحاولة، والوحدة التي تنفذ هذه الوظيفة تسمى: "rexec".

rexec

لا أنوي التعمق في شرح تنفيذ "rexec" لأن المبدأ تم شرحه بالفعل في النص السابق، وهذه الوحدة بحد ذاتها قد تم التخلي عنها، سأقوم فقط بتلخيص بعض الشيفرات المهمة بشكل بسيط لتسهيل الاطلاع.

# rexec.py
class RExec(ihooks._Verbose):
    ...
    nok_builtin_names = ('open', 'file', 'reload', '__import__')

    def __init__(self, hooks = None, verbose = 0):
        ...
        self.modules = {}
        ...
        self.make_builtin()
        self.make_initial_modules()
        self.make_sys()
        self.loader = RModuleLoader(self.hooks, verbose)
        self.importer = RModuleImporter(self.loader, verbose)

    def make_builtin(self):
        m = self.copy_except(__builtin__, self.nok_builtin_names)
        m.__import__ = self.r_import
        m.reload = self.r_reload
        m.open = m.file = self.r_open

    def add_module(self, mname):
        m = self.modules.get(mname)
        if m is None:
            self.modules[mname] = m = self.hooks.new_module(mname)
        m.__builtins__ = self.modules['__builtin__']
        return m

    def r_exec(self, code):
        m = self.add_module('__main__')
        exec code in m.__dict__

    def r_eval(self, code):
        m = self.add_module('__main__')
        return eval(code, m.__dict__)

    def r_execfile(self, file):
        m = self.add_module('__main__')
        execfile(file, m.__dict__)

تُقوم وظيفة r_execfile بتنفيذ الملف كـ __main__ وحدة، إلا أن __main__ تم تخصيصها. في self.add_module('__main__')، يتم ضبط m.__builtins__ = self.modules['__builtin__'] لإعداد وحدة __builtin__، والتي تم تخصيصها بواسطة make_builtin، حيث يتم استبدال داخل الوظيفة __import__، reload، وopen، وحذف نوع البيانات file. وبذلك، نستطيع التحكم في وصول الكود المراد تنفيذه لمساحة الأسماء المدمجة.

بالنسبة إلى بعض الوحدات المدمجة، قامت rexec أيضًا بالتخصيص لحماية الوصول غير الآمن، مثل وحدة sys، حيث تم الاحتفاظ بجزء من الكائنات فقط، وتم تحقيق تحميل الوحدة المخصصة بشكل أولوي أثناء import من خلال self.loader وself.importer المخصصة.

إذا كنت مهتمًا بتفاصيل الشيفرة البرمجية، يرجى الرجوع إلى الشيفرة المصدرية ذات الصلة.

فشل rexec

سابقًا تم ذكر أن "rexec" قد أصبحت بالفعل منبوذة بعد "Python 2.3" لأنه تبين أن هذه الطريقة ليست فعالة. لنلقِ نظرة سريعة إلى تاريخ هذا الأمر مع الفضول في قلوبنا:

تمّ تقديم تقرير حول الخللوقد أثارت مناقشة بين المطورين:

> it's never going to be safe, and I doubt it's very useful as long as it's not safe.

> Every change is a potential security hole.

> it's hard to predict what change is going to break it.

> I don't expect you'll ever reach the point where it'll be wise to advertise this as safe.  I certainly won't.

>  this is only a useful occupation if you expect to eventually reach a point where you expect that there aren't any security flaws left.  Jeremy & I both doubt that Python will ever reach that level, meaning that the whole exercise of fixing security flaws is a waste of time (if you know you *can't* make it safe, don't waste time trying).

> I agree (but I have said that in past) the best thing is to deprecate/rip out rexec.

> The code will still be in older versions if someone decides to pick it up and work on it as a separate project.

سبب هذا الخطأ هو وجود فئة جديدة "object" في Python والتي تؤدي إلى عدم قدرة rexec على العمل بشكل صحيح. لذا عبّر المطورون عن صعوبة تجنب هذا النوع من الحالات في المستقبل المرئي، حيث إن أي تعديل قد يؤدي إلى ظهور ثغرات في rexec أو عدم قدرته على العمل بشكل صحيح، أو حتى تجاوز الحدود الأمنية. عموماً، يصعب تحقيق رؤية بيئة آمنة خالية من الثغرات دون إصلاحات مستمرة وإضافة عديدة، مما يؤدي إلى إضاعة الكثير من الوقت. وبنهاية المطاف، تم التخلي عن هذه الوحدة rexec، دون توفير Python لميزات مماثلة. ومع ذلك، فتعيين __builtins__، نظرًا لقضايا التوافق وما إلى ذلك، تم الاحتفاظ به.

في ما يقرب من عام 2010، قام مبرمج بإصدار pysandboxنحن ملتزمون بتوفير بيئة رملية برمجية بلغة Python يمكنها استبدال rexec. لكن بعد 3 سنوات، قرر المؤلف التخلي بشكل طوعي عن هذا المشروع وشرح بالتفصيل لماذا اعتبر أن المشروع قد فشل: The pysandbox project is brokenترجمة النص إلى اللغة العربية: ، وقامت كذلك عدد من الكتاب الآخرين بتلخيص فشل هذا المشروع: فشل بيساندبوإكسإذا كنت مهتمًا، يمكنك الرجوع مباشرة إلى النص الأصلي، وها هنا بعض الملخصات لمساعدتك في الفهم:

After having work during 3 years on a pysandbox project to sandbox untrusted code, I now reached a point where I am convinced that pysandbox is broken by design. Different developers tried to convinced me before that pysandbox design is unsafe, but I had to experience it myself to be convineced.

I now agree that putting a sandbox in CPython is the wrong design. There are too many ways to escape the untrusted namespace using the various introspection features of the Python language. To guarantee the [safety] of a security product, the code should be [carefully] audited and the code to review must be as small as possible. Using pysandbox, the "code" is the whole Python core which is a really huge code base. For example, the Python and Objects directories of Python 3.4 contain more than 126,000 lines of C code.

The security of pysandbox is the security of its weakest part. A single bug is enough to escape the whole sandbox.

pysandbox cannot be used in practice. To protect the untrusted namespace, pysandbox installs a lot of different protections. Because of all these protections, it becomes hard to write Python code. Basic features like "del dict[key]" are denied. Passing an object to a sandbox is not possible to sandbox, pysandbox is unable to proxify arbitary objects. For something more complex than evaluating "1+(2*3)", pysandbox cannot be used in practice, because of all these protections.

يعتقد مؤلف pysandbox أن وضع بيئة رملية في Python خطأ في التصميم، حيث يمكن الهروب منها بطرق كثيرة، وتوفر Python ميزات لغوية غنية، وكمية الشفرة في مصدر CPython كبيرة للغاية، ومن الصعب توفير مستوى كاف من الأمان. وكانت عملية تطوير pysandbox عبارة عن تطبيق للتصليحات بشكل مستمر، وكانت هناك الكثير من التقييدات والتحديات، لدرجة أن المؤلف يعتقد الآن أنه لم يعد من الممكن استخدام pysandbox عمليًا، لأن العديد من ميزات اللغة والوظائف تم تقييدها وعدم إمكانية استخدامها، مثل الفعل البسيط del dict[key].

التنفيذ المقيد، أين المخرج؟

نظرًا لأن طرق مثل rexec و pysandbox التي تقوم بتوفير بيئات رملية باستخدام تصحيح Python لم تعد فعالة، فأنا لا أستطيع إلا أن أتساءل: كيف يمكنني توفير بيئة رملية فعالة لـ Python؟

في هذا المكان، واصلت جمع بعض الطرق التي يمكن تنفيذها أو الحالات الأخرى، لسهولة الرجوع والاطلاع:

  • PyPy(https://foss.heptapod.net/pypy/pypy/-/tree/branch/sandbox-2)قدَّمت وظيفة الصندوق الرملي، مع توفير sandboxlibيمكنك تجميع PyPy بنسخة بيئة رملية بنفسك. إذا كنت مهتمًا، يمكنك تجربة التكوين بنفسك، استشر بعض التوجيهاتتنفيذ PyPy يكون عن طريق إنشاء عملية فرعية، حيث يتم إعادة توجيه جميع إدخالاتها وإخراجاتها واستدعاءات النظام إلى عملية خارجية تتحكم في هذه الأذونات، ويمكن أيضًا التحكم في استخدام الذاكرة والمعالج. يجب ملاحظة أن هذا الفرع لم يتم تقديم تعديلات جديدة لبعض الوقت، لذا يُنصح باستخدامه بحذر.

استخدم أدوات بيئة الرمل الخاصة بنظام التشغيل. seccompمرحبًا! إليك الترجمة:

هو أداة أمان حوسبة تُوفَّرها نواة Linux، libseccompقدمت روابط Python التي يمكن تضمينها في الرمز للاستخدام؛ أو استخدام أدوات تعتمد على seccomp لتنفيذ الرمز، مثل Firejailأب آرمورهو وحدة أمان لنواة Linux تسمح للمسؤولين بالتحكم في الموارد والوظائف التي يمكن للبرامج الوصول إليها، مما يحمي نظام التشغيل. codejailهو بيئة رملية للـ Python مبنية على AppArmor، يمكنك تجربتها إذا كنت مهتمًا. وهناك العديد من الأدوات المماثلة، لن أذكرها جميعًا هنا.

استخدم بيئة الرمل أو الحاوية الافتراضية. صندوق الرمل لنظام Windows، LXC, Dockerانتظر قليلا، لن أقوم بتوضيح ذلك هنا.

التلخيص

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

Original: https://wiki.disenone.site/ar

This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.

هذا المنشور تم ترجمته باستخدام ChatGPT، الرجاء تقديم ملاحظاتأشير إلى أي نقص أو غياب.