33import sys
44import tempfile
55import unittest
6+ from unittest import mock
67
78from django .contrib .auth .models import User
89from django .core import signing
@@ -83,17 +84,16 @@ def test_generate_stats_no_profiler(self):
8384 response = HttpResponse ()
8485 self .assertIsNone (self .panel .generate_stats (self .request , response ))
8586
86- @override_settings (
87- DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : tempfile .gettempdir ()}
88- )
8987 def test_generate_stats_signed_path (self ):
90- response = self .panel .process_request (self .request )
91- self .panel .generate_stats (self .request , response )
92- path = self .panel .prof_file_path
93- self .assertTrue (path )
94- # Check that it's a valid signature
95- filename = signing .loads (path )
96- self .assertTrue (filename .endswith (".prof" ))
88+ with tempfile .TemporaryDirectory () as tmpdir :
89+ with self .settings (DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : tmpdir }):
90+ response = self .panel .process_request (self .request )
91+ self .panel .generate_stats (self .request , response )
92+ path = self .panel .prof_file_path
93+ self .assertTrue (path )
94+ # Check that it's a valid signature
95+ filename = signing .loads (path )
96+ self .assertTrue (filename .endswith (".prof" ))
9797
9898 def test_generate_stats_no_root (self ):
9999 response = self .panel .process_request (self .request )
@@ -112,6 +112,20 @@ def test_generate_stats_no_root_func(self):
112112 self .panel .generate_stats (self .request , response )
113113 self .assertNotIn ("func_list" , self .panel .get_stats ())
114114
115+ @mock .patch ("cProfile.Profile.dump_stats" )
116+ def test_generate_stats_oserror (self , mock_dump_stats ):
117+ mock_dump_stats .side_effect = OSError
118+ with tempfile .TemporaryDirectory () as tmpdir :
119+ with self .settings (DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : tmpdir }):
120+ response = self .panel .process_request (self .request )
121+ with self .assertLogs (
122+ "debug_toolbar.panels.profiling" , level = "ERROR"
123+ ) as cm :
124+ self .panel .generate_stats (self .request , response )
125+ self .assertIn ("Failed to dump profiling stats" , cm .output [0 ])
126+ # Ensure prof_file_path is not set/updated if dump fails
127+ self .assertFalse (hasattr (self .panel , "prof_file_path" ))
128+
115129
116130@override_settings (
117131 DEBUG = True , DEBUG_TOOLBAR_PANELS = ["debug_toolbar.panels.profiling.ProfilingPanel" ]
@@ -172,3 +186,35 @@ def test_download_missing_file(self):
172186 path = signing .dumps ("missing.prof" )
173187 response = self .client .get (url , {"path" : path })
174188 self .assertEqual (response .status_code , 404 )
189+
190+ def test_download_path_traversal (self ):
191+ with override_settings (
192+ DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : self .root }
193+ ):
194+ url = reverse ("djdt:debug_toolbar_download_prof_file" )
195+ # Sign a filename that traverses safely out of the root
196+ path = signing .dumps ("../passwd" )
197+ response = self .client .get (url , {"path" : path })
198+ self .assertEqual (response .status_code , 404 )
199+
200+ def test_download_absolute_path (self ):
201+ with override_settings (
202+ DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : self .root }
203+ ):
204+ url = reverse ("djdt:debug_toolbar_download_prof_file" )
205+ # Create a file outside the root and try to access it via absolute path
206+ with tempfile .NamedTemporaryFile () as tmp :
207+ path = signing .dumps (tmp .name )
208+ response = self .client .get (url , {"path" : path })
209+ self .assertEqual (response .status_code , 404 )
210+
211+ def test_download_recursive_traversal (self ):
212+ with override_settings (
213+ DEBUG_TOOLBAR_CONFIG = {"PROFILER_PROFILE_ROOT" : self .root }
214+ ):
215+ url = reverse ("djdt:debug_toolbar_download_prof_file" )
216+ # Try a convoluted path that resolves outside
217+ # e.g. root/subdir/../../outside_root
218+ path = signing .dumps (os .path .join ("subdir" , ".." , ".." , "passwd" ))
219+ response = self .client .get (url , {"path" : path })
220+ self .assertEqual (response .status_code , 404 )
0 commit comments