diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000000..3432a1cbb61b9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,221 @@ +# Git +.git +.gitignore +.gitattributes +.hg/ +.svn/ + +# Build artifacts +*.o +*.lto +*.a +*.so +*.so.* +*.dylib +*.dSYM +*.exe +*.dll +*.pyd +*.wasm +*.gc?? +*.prebolt +*.fdata +*.dyn + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.pyo + +# Profiling data +*.profraw +*.profclang? +*.profdata +default.profraw +gmon.out + +# Testing artifacts +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.coverage +htmlcov/ +*.cover +.hypothesis/ + +# IDE and editors +.vscode/ +.idea/ +.vs/ +.cache/ +*.swp +*.swo +*~ +*.iml +tags +TAGS + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Documentation +Doc/build/ +Doc/_build/ +Doc/venv/ +Doc/.venv/ +Doc/env/ +Doc/.env/ + +# Virtual environments +venv/ +env/ +ENV/ + +# Temporary and backup files +*.tmp +*.bak +*.log +*.orig +*.rej +tmp/ +temp/ +.gdb_history +.purify +core + +# Root-level build artifacts +/_bootstrap_python +/Makefile +/Makefile.pre +/build/ +/builddir/ +/config.cache +/config.log +/config.status +/config.status.lineno +/.ccache +/cross-build/ +/jit_stencils*.h +/platform +/profile-clean-stamp +/profile-run-stamp +/profile-bolt-stamp +/profile-gen-stamp +/pybuilddir.txt +/pyconfig.h +/python-config +/python-config.py +/python.bat +/python-gdb.py +/python.exe-gdb.py +/reflog.txt +/coverage/ +/externals/ +/htmlcov/ +/python +python.exe.lto/ +*.framework/ + +# iOS and Apple +/iOSTestbed.* +Apple/iOS/Frameworks/ +Apple/iOS/Resources/Info.plist +Apple/testbed/build +Apple/testbed/Python.xcframework/*/bin +Apple/testbed/Python.xcframework/*/include +Apple/testbed/Python.xcframework/*/lib +Apple/testbed/Python.xcframework/*/Python.framework +Apple/testbed/*Testbed.xcodeproj/project.xcworkspace +Apple/testbed/*Testbed.xcodeproj/xcuserdata + +# Mac +Mac/Makefile +Mac/PythonLauncher/Info.plist +Mac/PythonLauncher/Makefile +Mac/PythonLauncher/Python Launcher +Mac/PythonLauncher/Python Launcher.app/* +Mac/Resources/app/Info.plist +Mac/Resources/framework/Info.plist +Mac/pythonw + +# Modules +Modules/Setup.bootstrap +Modules/Setup.config +Modules/Setup.local +Modules/Setup.stdlib +Modules/config.c +Modules/ld_so_aix +Modules/python.exp + +# Programs +Programs/_freeze_module +Programs/_testembed + +# PC (Windows) +PC/python_nt*.h +PC/pythonnt_rc*.h +PC/*/*.exp +PC/*/*.lib +PC/*/*.bsc +PC/*/*.dll +PC/*/*.pdb +PC/*/*.user +PC/*/*.ncb +PC/*/*.suo +PC/*/Win32-temp-* +PC/*/x64-temp-* +PC/*/amd64 + +# PCbuild (Windows build) +PCbuild/*.user +PCbuild/*.suo +PCbuild/*.*sdf +PCbuild/*-pgi +PCbuild/*-pgo +PCbuild/*.VC.db +PCbuild/*.VC.opendb +PCbuild/amd64/ +PCbuild/arm32/ +PCbuild/arm64/ +PCbuild/obj/ +PCbuild/win32/ + +# Tools +Tools/unicode/data/ +Tools/msi/obj +Tools/ssl/amd64 +Tools/ssl/win32 +Tools/freeze/test/outdir + +# Misc +Misc/python.pc +Misc/python-embed.pc +Misc/python-config.sh + +# Include +Include/pydtrace_probes.h + +# Lib +Lib/site-packages/* +!Lib/site-packages/README.txt +Lib/test/data/* +!Lib/test/data/README + +# Python frozen modules +Python/frozen_modules/*.h +Python/frozen_modules/MANIFEST + +# Claude config +/.claude/ +CLAUDE.local.md + +# Docker files (avoid recursive inclusion) +.dockerignore +Dockerfile + +# Local development +foo.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000000..0ab83a6e9b2058 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,120 @@ +FROM debian:trixie-slim + +ENV PATH=/usr/local/bin:$PATH +ENV PYTHONDONTWRITEBYTECODE=1 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + netbase \ + tzdata \ + ; \ + apt-get dist-clean + +COPY . /usr/src/python + +RUN set -eux; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + dpkg-dev \ + gcc \ + gnupg \ + libbluetooth-dev \ + libbz2-dev \ + libc6-dev \ + libdb-dev \ + libffi-dev \ + libgdbm-dev \ + liblzma-dev \ + libncursesw5-dev \ + libreadline-dev \ + libsqlite3-dev \ + libssl-dev \ + libzstd-dev \ + make \ + tk-dev \ + uuid-dev \ + wget \ + xz-utils \ + zlib1g-dev \ + ; \ + \ + cd /usr/src/python; \ + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ + ./configure \ + --build="$gnuArch" \ + --enable-loadable-sqlite-extensions \ + --enable-optimizations \ + --enable-option-checking=fatal \ + --enable-shared \ + $(test "${gnuArch%%-*}" != 'riscv64' && echo '--with-lto') \ + --with-ensurepip \ + ; \ + nproc="$(nproc)"; \ + EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"; \ + LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"; \ + LDFLAGS="${LDFLAGS:--Wl},--strip-all"; \ + arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \ + case "$arch" in \ + amd64|arm64) \ + EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"; \ + ;; \ + i386) \ + ;; \ + *) \ + EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer"; \ + ;; \ + esac; \ + make -j "$nproc" \ + "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \ + "LDFLAGS=${LDFLAGS:-}" \ + ; \ + rm python; \ + make -j "$nproc" \ + "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \ + "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" \ + python \ + ; \ + make install; \ + \ + cd /; \ + rm -rf /usr/src/python; \ + \ + find /usr/local -depth \ + \( \ + \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ + -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \ + \) -exec rm -rf '{}' + \ + ; \ + \ + ldconfig; \ + \ + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \ + | sort -u \ + | xargs -rt dpkg-query --search \ + | awk 'sub(":$", "", $1) { print $1 }' \ + | sort -u \ + | xargs -r apt-mark manual \ + ; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + apt-get dist-clean; \ + \ + export PYTHONDONTWRITEBYTECODE=1; \ + python3 --version; \ + pip3 --version + +RUN set -eux; \ + for src in idle3 pip3 pydoc3 python3 python3-config; do \ + dst="$(echo "$src" | tr -d 3)"; \ + [ -s "/usr/local/bin/$src" ]; \ + [ ! -e "/usr/local/bin/$dst" ]; \ + ln -svT "$src" "/usr/local/bin/$dst"; \ + done + +CMD ["python3"] diff --git a/Include/internal/pycore_context.h b/Include/internal/pycore_context.h index c77ef7910c09aa..d7f1f1f73c1e64 100644 --- a/Include/internal/pycore_context.h +++ b/Include/internal/pycore_context.h @@ -26,6 +26,9 @@ struct _pycontextobject { PyHamtObject *ctx_vars; PyObject *ctx_weakreflist; int ctx_entered; + struct { + unsigned int deserialization_taint_counter; + } security_ctx; }; @@ -55,5 +58,10 @@ struct _pycontexttokenobject { // Export for '_testcapi' shared extension PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void); +// Deserialization guard API +PyAPI_FUNC(int) _PyContext_IncrementDeserializationTaint(void); +PyAPI_FUNC(int) _PyContext_DecrementDeserializationTaint(void); +PyAPI_FUNC(int) _PyContext_IsDeserializationTainted(void); + #endif /* !Py_INTERNAL_CONTEXT_H */ diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index a08038b5dbd407..deb8f124f842ec 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -1296,5 +1296,79 @@ def test_hamt_getitem_1(self): h[AA] +class DeserializationGuardTest(unittest.TestCase): + @support.cpython_only + def test_deserialization_taint_basic(self): + import _testinternalcapi + + ctx = contextvars.copy_context() + self.assertFalse(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_increment_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_decrement_taint() + self.assertFalse(_testinternalcapi.context_is_tainted()) + + @support.cpython_only + def test_deserialization_taint_underflow(self): + import _testinternalcapi + + self.assertFalse(_testinternalcapi.context_is_tainted()) + + with self.assertRaisesRegex(RuntimeError, 'deserialization taint counter underflow'): + _testinternalcapi.context_decrement_taint() + + @support.cpython_only + def test_deserialization_taint_inheritance_copy(self): + import _testinternalcapi + + _testinternalcapi.context_increment_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + ctx = contextvars.copy_context() + + def check_taint(): + self.assertTrue(_testinternalcapi.context_is_tainted()) + + ctx.run(check_taint) + + _testinternalcapi.context_decrement_taint() + + @support.cpython_only + def test_deserialization_taint_inheritance_new(self): + import _testinternalcapi + + _testinternalcapi.context_increment_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + ctx = contextvars.Context() + + def check_taint(): + self.assertTrue(_testinternalcapi.context_is_tainted()) + + ctx.run(check_taint) + + _testinternalcapi.context_decrement_taint() + + @support.cpython_only + def test_deserialization_taint_nested(self): + import _testinternalcapi + + self.assertFalse(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_increment_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_increment_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_decrement_taint() + self.assertTrue(_testinternalcapi.context_is_tainted()) + + _testinternalcapi.context_decrement_taint() + self.assertFalse(_testinternalcapi.context_is_tainted()) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index e2384b33345a45..58eba2a5908f90 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -15,7 +15,7 @@ import doctest import unittest from test import support -from test.support import cpython_only, import_helper, os_helper +from test.support import cpython_only, import_helper, os_helper, threading_helper from test.support.import_helper import ensure_lazy_imports from test.pickletester import AbstractHookTests @@ -767,5 +767,137 @@ def load_tests(loader, tests, pattern): return tests +class DeserializationGuardTests(unittest.TestCase): + @support.cpython_only + def test_os_system_blocked_during_pickle(self): + import os + + class MaliciousOsSystem: + def __reduce__(self): + return (os.system, ('echo test',)) + + malicious = MaliciousOsSystem() + data = pickle.dumps(malicious) + + with self.assertRaisesRegex(RuntimeError, 'disabled during deserialization'): + pickle.loads(data) + + def test_os_system_allowed_outside_pickle(self): + import os + + os.system('true') + + @support.cpython_only + def test_taint_cleared_on_error(self): + import _testinternalcapi + + data = b'\x80\x05\x95\x00\x00\x00\x00\x00\x00\x00INVALID' + + self.assertFalse(_testinternalcapi.context_is_tainted()) + + with self.assertRaises(pickle.UnpicklingError): + pickle.loads(data) + + self.assertFalse(_testinternalcapi.context_is_tainted()) + + @support.cpython_only + def test_taint_cleared_on_success(self): + import _testinternalcapi + + data = pickle.dumps({"key": "value"}) + + self.assertFalse(_testinternalcapi.context_is_tainted()) + + result = pickle.loads(data) + + self.assertFalse(_testinternalcapi.context_is_tainted()) + self.assertEqual(result, {"key": "value"}) + + @support.cpython_only + @threading_helper.requires_working_threading() + def test_with_asyncio(self): + import asyncio + import os + + class MaliciousAsync: + def __reduce__(self): + return (os.system, ("echo test",)) + + async def test_coro(): + malicious = MaliciousAsync() + data = pickle.dumps(malicious) + + with self.assertRaisesRegex(RuntimeError, 'disabled during deserialization'): + pickle.loads(data) + + old_policy = support.maybe_get_event_loop_policy() + try: + asyncio.run(test_coro()) + finally: + asyncio.events._set_event_loop_policy(old_policy) + + @support.cpython_only + def test_subprocess_blocked(self): + import subprocess + + class MaliciousSubprocess: + def __reduce__(self): + return (subprocess.Popen, (['echo', 'test'],)) + + data = pickle.dumps(MaliciousSubprocess()) + with self.assertRaisesRegex(RuntimeError, 'subprocess.Popen is disabled'): + pickle.loads(data) + + @support.cpython_only + def test_ctypes_dlopen_blocked(self): + import ctypes + + class MaliciousCtypes: + def __reduce__(self): + return (ctypes.CDLL, ('libc.dylib',)) + + data = pickle.dumps(MaliciousCtypes()) + with self.assertRaisesRegex(RuntimeError, 'ctypes.dlopen is disabled'): + pickle.loads(data) + + @support.cpython_only + def test_os_remove_blocked(self): + import os + + class MaliciousRemove: + def __reduce__(self): + return (os.remove, ('/tmp/test',)) + + data = pickle.dumps(MaliciousRemove()) + with self.assertRaisesRegex(RuntimeError, 'os.remove is disabled'): + pickle.loads(data) + + @support.cpython_only + def test_os_chmod_blocked(self): + import os + + class MaliciousChmod: + def __reduce__(self): + return (os.chmod, ('/tmp/test', 0o777)) + + data = pickle.dumps(MaliciousChmod()) + with self.assertRaisesRegex(RuntimeError, 'os.chmod is disabled'): + pickle.loads(data) + + @support.cpython_only + @threading_helper.requires_working_threading() + def test_thread_creation_blocked(self): + import _thread + import time + + class MaliciousThread: + def __reduce__(self): + return (_thread.start_new_thread, (time.sleep, (0,))) + + data = pickle.dumps(MaliciousThread()) + with self.assertRaisesRegex(RuntimeError, '_thread.start_new_thread is disabled'): + pickle.loads(data) + + if __name__ == "__main__": unittest.main() diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 2e74d5688629ff..a6a2acb9dbf5ab 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -11,6 +11,7 @@ #include "Python.h" #include "pycore_bytesobject.h" // _PyBytesWriter #include "pycore_ceval.h" // _Py_EnterRecursiveCall() +#include "pycore_context.h" // _PyContext_IncrementDeserializationTaint() #include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION() #include "pycore_long.h" // _PyLong_AsByteArray() #include "pycore_moduleobject.h" // _PyModule_GetState() @@ -6891,6 +6892,10 @@ load(PickleState *st, UnpicklerObject *self) PyObject *tmp; char *s = NULL; + if (_PyContext_IncrementDeserializationTaint() < 0) { + return NULL; + } + self->num_marks = 0; self->stack->mark_set = 0; self->stack->fence = 0; @@ -7019,10 +7024,17 @@ load(PickleState *st, UnpicklerObject *self) Py_CLEAR(self->persistent_load); PDATA_POP(st, self->stack, value); + + if (_PyContext_DecrementDeserializationTaint() < 0) { + Py_DECREF(value); + return NULL; + } + return value; error: Py_CLEAR(self->persistent_load); + _PyContext_DecrementDeserializationTaint(); return NULL; } diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index f84cf1a4263a2d..f7ff25eedeaca6 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1539,6 +1539,34 @@ new_hamt(PyObject *self, PyObject *args) } +static PyObject* +context_is_tainted(PyObject *self, PyObject *Py_UNUSED(args)) +{ + int is_tainted = _PyContext_IsDeserializationTainted(); + return PyBool_FromLong(is_tainted); +} + + +static PyObject* +context_increment_taint(PyObject *self, PyObject *Py_UNUSED(args)) +{ + if (_PyContext_IncrementDeserializationTaint() < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + +static PyObject* +context_decrement_taint(PyObject *self, PyObject *Py_UNUSED(args)) +{ + if (_PyContext_DecrementDeserializationTaint() < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static PyObject* dict_getitem_knownhash(PyObject *self, PyObject *args) { @@ -2432,6 +2460,9 @@ static PyMethodDef module_functions[] = { {"pymem_getallocatorsname", test_pymem_getallocatorsname, METH_NOARGS}, {"get_object_dict_values", get_object_dict_values, METH_O}, {"hamt", new_hamt, METH_NOARGS}, + {"context_is_tainted", context_is_tainted, METH_NOARGS}, + {"context_increment_taint", context_increment_taint, METH_NOARGS}, + {"context_decrement_taint", context_decrement_taint, METH_NOARGS}, {"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS}, {"create_interpreter", _PyCFunction_CAST(create_interpreter), METH_VARARGS | METH_KEYWORDS}, diff --git a/Python/context.c b/Python/context.c index d1f8b7c2482181..8b774bbe1af752 100644 --- a/Python/context.c +++ b/Python/context.c @@ -9,6 +9,17 @@ #include "pycore_pyerrors.h" #include "pycore_pystate.h" // _PyThreadState_GET() +#include // strcmp() +#include // tolower() + + +typedef enum { + HARDEN_MODE_OFF, + HARDEN_MODE_WARN, + HARDEN_MODE_ON +} HardenMode; + +static HardenMode _harden_mode = HARDEN_MODE_ON; #include "clinic/context.c.h" @@ -39,6 +50,25 @@ module _contextvars return err_ret; \ } +#define HARDEN_ERROR_OR_WARN(event) \ + do { \ + if (_harden_mode == HARDEN_MODE_WARN) { \ + if (PyErr_WarnFormat( \ + PyExc_RuntimeWarning, 1, \ + "Insecure %s during deserialization was detected", \ + event) < 0) \ + { \ + return -1; \ + } \ + return 0; \ + } \ + else { \ + PyErr_Format(PyExc_RuntimeError, \ + "%s is disabled during deserialization", event); \ + return -1; \ + } \ + } while (0) + /////////////////////////// Context API @@ -257,6 +287,57 @@ PyContext_Exit(PyObject *octx) } +int +_PyContext_IncrementDeserializationTaint(void) +{ + PyContext *ctx = context_get(); + if (ctx == NULL) { + return -1; + } + + if (ctx->security_ctx.deserialization_taint_counter == UINT_MAX) { + PyErr_SetString(PyExc_RuntimeError, + "deserialization taint counter overflow"); + return -1; + } + ctx->security_ctx.deserialization_taint_counter++; + + return 0; +} + + +int +_PyContext_DecrementDeserializationTaint(void) +{ + PyContext *ctx = context_get(); + if (ctx == NULL) { + return -1; + } + + if (ctx->security_ctx.deserialization_taint_counter == 0) { + PyErr_SetString(PyExc_RuntimeError, + "deserialization taint counter underflow"); + return -1; + } + + ctx->security_ctx.deserialization_taint_counter--; + + return 0; +} + + +int +_PyContext_IsDeserializationTainted(void) +{ + PyContext *ctx = context_get(); + if (ctx == NULL) { + return 0; + } + + return ctx->security_ctx.deserialization_taint_counter != 0; +} + + PyObject * PyContextVar_New(const char *name, PyObject *def) { @@ -422,6 +503,18 @@ class _contextvars.Context "PyContext *" "&PyContext_Type" #define _PyContext_CAST(op) ((PyContext *)(op)) +static inline void +_context_propagate_taint(PyContext *ctx) +{ + PyThreadState *ts = _PyThreadState_GET(); + if (ts != NULL && ts->context != NULL) { + PyContext *current = (PyContext *)ts->context; + ctx->security_ctx.deserialization_taint_counter = + current->security_ctx.deserialization_taint_counter; + } +} + + static inline PyContext * _context_alloc(void) { @@ -437,6 +530,7 @@ _context_alloc(void) ctx->ctx_prev = NULL; ctx->ctx_entered = 0; ctx->ctx_weakreflist = NULL; + ctx->security_ctx.deserialization_taint_counter = 0; return ctx; } @@ -456,6 +550,8 @@ context_new_empty(void) return NULL; } + _context_propagate_taint(ctx); + _PyObject_GC_TRACK(ctx); return ctx; } @@ -471,6 +567,8 @@ context_new_from_vars(PyHamtObject *vars) ctx->ctx_vars = (PyHamtObject*)Py_NewRef(vars); + _context_propagate_taint(ctx); + _PyObject_GC_TRACK(ctx); return ctx; } @@ -1354,6 +1452,101 @@ get_token_missing(void) /////////////////////////// +static HardenMode +_parse_harden_mode(void) +{ + const char *env_value = getenv("PYTHONHARDENMODE"); + if (env_value == NULL) { + return HARDEN_MODE_ON; + } + + char lower_value[16] = {0}; + size_t i = 0; + while (env_value[i] != '\0' && i < sizeof(lower_value) - 1) { + lower_value[i] = tolower((unsigned char)env_value[i]); + i++; + } + lower_value[i] = '\0'; + + if (strcmp(lower_value, "off") == 0) { + return HARDEN_MODE_OFF; + } + else if (strcmp(lower_value, "warn") == 0) { + return HARDEN_MODE_WARN; + } + else { + return HARDEN_MODE_ON; + } +} + + +static int +_deserialization_guard_audit_hook(const char *event, PyObject *args, + void *userData) +{ + if (!_PyContext_IsDeserializationTainted()) { + return 0; + } + + if ((strncmp(event, "os.", 3) == 0 && strcmp(event, "os.listdir") != 0) || + strncmp(event, "ctypes.", 7) == 0 || + strncmp(event, "_thread.", 8) == 0 || + strncmp(event, "_winapi.", 8) == 0 || + strncmp(event, "_posixsubprocess.", 17) == 0 || + strncmp(event, "socket.", 7) == 0 || + strncmp(event, "sqlite3.", 8) == 0 || + strncmp(event, "fcntl.", 6) == 0 || + strncmp(event, "signal.", 7) == 0 || + strncmp(event, "winreg.", 7) == 0 || + strncmp(event, "syslog.", 7) == 0 || + strncmp(event, "cpython.", 8) == 0 || + strncmp(event, "gc.", 3) == 0 || + (strncmp(event, "sys.", 4) == 0 && strcmp(event, "sys._getframemodulename") != 0) || + strncmp(event, "resource.", 9) == 0 || + strncmp(event, "shutil.", 7) == 0 || + strncmp(event, "pathlib.", 8) == 0 || + strncmp(event, "tempfile.", 9) == 0 || + strncmp(event, "ftplib.", 7) == 0 || + strncmp(event, "http.", 5) == 0 || + strncmp(event, "imaplib.", 8) == 0 || + strncmp(event, "nntplib.", 8) == 0 || + strncmp(event, "poplib.", 7) == 0 || + strncmp(event, "smtplib.", 8) == 0 || + strncmp(event, "telnetlib.", 10) == 0 || + strncmp(event, "urllib.", 7) == 0 || + strncmp(event, "msvcrt.", 7) == 0 || + strncmp(event, "subprocess.", 11) == 0 || + strncmp(event, "webbrowser.", 11) == 0 || + strncmp(event, "pty.", 4) == 0 || + strncmp(event, "ensurepip.", 10) == 0 || + strncmp(event, "glob.", 5) == 0 || + strncmp(event, "mmap.", 5) == 0 || + strncmp(event, "setopencodehook", 15) == 0) + { + HARDEN_ERROR_OR_WARN(event); + } + + if (strcmp(event, "open") == 0) { + if (PyTuple_Size(args) >= 2) { + PyObject *mode = PyTuple_GetItem(args, 1); + if (mode != NULL && PyUnicode_Check(mode)) { + const char *mode_str = PyUnicode_AsUTF8(mode); + if (mode_str != NULL && + (strchr(mode_str, 'w') != NULL || + strchr(mode_str, 'a') != NULL || + strchr(mode_str, 'x') != NULL || + strchr(mode_str, '+') != NULL)) + { + HARDEN_ERROR_OR_WARN("open with write mode"); + } + } + } + } + + return 0; +} + + PyStatus _PyContext_Init(PyInterpreterState *interp) { @@ -1367,5 +1560,13 @@ _PyContext_Init(PyInterpreterState *interp) } Py_DECREF(missing); + _harden_mode = _parse_harden_mode(); + + if (_harden_mode != HARDEN_MODE_OFF) { + if (PySys_AddAuditHook(_deserialization_guard_audit_hook, NULL) < 0) { + return _PyStatus_ERR("can't add deserialization guard audit hook"); + } + } + return _PyStatus_OK(); }