From 16875c747c7f122e84e18f74374793d789b7991a Mon Sep 17 00:00:00 2001 From: bctiemann Date: Tue, 14 Jul 2026 15:44:19 -0400 Subject: [PATCH] Closes #22654: Redact install paths from debug tracebacks (#22655) --- netbox/extras/models/configs.py | 21 ++++++++++++++++++++- netbox/extras/tests/test_models.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index f8e906663..875ea854b 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -1,3 +1,6 @@ +import os +import re +import sys import traceback import jsonschema @@ -333,7 +336,23 @@ class ConfigTemplate( is returned. """ if self.debug: - return ''.join(traceback.format_exception(exc)) + # Strip deployment-specific path prefixes from File "..." lines to avoid disclosing + # the server's filesystem layout. install_root covers all NetBox source files plus + # any venv co-located inside the repo. When the venv lives outside the repo + # (the typical production pattern, e.g. ~/.venv/netbox/), sys.prefix differs from + # sys.base_prefix and the venv root is stripped separately so that the deployment + # user's home directory is not exposed. Stdlib paths not under either prefix are + # left as-is — they reveal only standard OS locations, not deployment structure. + install_root = os.path.dirname(settings.BASE_DIR) + os.sep + prefixes_to_strip = [install_root] + if sys.prefix != sys.base_prefix: + venv_root = sys.prefix + os.sep + if venv_root != install_root: + prefixes_to_strip.append(venv_root) + tb = ''.join(traceback.format_exception(exc)) + for prefix in prefixes_to_strip: + tb = re.sub(r'(File ")' + re.escape(prefix), r'\1', tb) + return tb if isinstance(exc, TemplateError): parts = [f"{type(exc).__name__}: {exc}"] if getattr(exc, 'name', None): diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index ce7940a93..0e48361b2 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,9 +1,12 @@ import io +import os +import sys import tempfile from pathlib import Path from types import SimpleNamespace from unittest.mock import patch +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile from django.core.files.storage import Storage @@ -1200,6 +1203,32 @@ class ConfigTemplateDebugTestCase(TestCase): with self.assertRaises(TemplateSyntaxError): render_jinja2("{% debug %}", {}, debug=False) + def test_format_render_error_debug_redacts_install_path(self): + """format_render_error() strips the repo install-path prefix from debug tracebacks.""" + t = ConfigTemplate(name='redact-test', template_code='hello', debug=True) + try: + raise ValueError("deliberate test error") + except ValueError as exc: + result = t.format_render_error(exc) + install_root = os.path.dirname(settings.BASE_DIR) + os.sep + self.assertIn('Traceback', result) + self.assertNotIn(install_root, result) + # Also verify the venv prefix is stripped when running inside a virtualenv. + if sys.prefix != sys.base_prefix: + venv_root = sys.prefix + os.sep + if venv_root != install_root: + self.assertNotIn(venv_root, result) + + def test_format_render_error_non_debug_returns_concise_message(self): + """format_render_error() returns a one-line message (no traceback) when debug=False.""" + t = ConfigTemplate(name='nodebug-test', template_code='hello', debug=False) + try: + raise TemplateError("bad template") + except TemplateError as exc: + result = t.format_render_error(exc) + self.assertNotIn('Traceback', result) + self.assertIn('TemplateError', result) + class JinjaEnvFilterTestCase(TestCase): """