Opened 4 weeks ago
Last modified 4 weeks ago
#35930 new Cleanup/optimization
Database password visible on debug page in local variable
Reported by: | bytej4ck | Owned by: | Baha Sdtbekov |
---|---|---|---|
Component: | Error reporting | Version: | dev |
Severity: | Normal | Keywords: | db, password, exposed |
Cc: | bytej4ck | Triage Stage: | Accepted |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
In debug page view, secrets are not visible due to masked with '*'. When there is mysql db connection error due to unreachable db server: self.connection = self.get_new_connection(conn_params) exposes db password under Local vars
dropdown.
conn_params {'charset': 'utf8mb4', 'client_flag': 2, 'conv': {0: <class 'decimal.Decimal'>, 1: <class 'int'>, 2: <class 'int'>, 3: <class 'int'>, 4: <class 'float'>, 5: <class 'float'>, 7: <function DateTime_or_None at 0x7f6218e5b490>, 8: <class 'int'>, 9: <class 'int'>, 10: <function Date_or_None at 0x7f6218e5b640>, 11: <function typecast_time at 0x7f6219d803a0>, 12: <function DateTime_or_None at 0x7f6218e5b490>, 13: <class 'int'>, 15: <class 'bytes'>, 245: <class 'bytes'>, 246: <class 'decimal.Decimal'>, 249: <class 'bytes'>, 250: <class 'bytes'>, 251: <class 'bytes'>, 252: <class 'bytes'>, 253: <class 'bytes'>, 254: <class 'bytes'>, <class 'array.array'>: <function array2Str at 0x7f6218e84160>, <class 'decimal.Decimal'>: <function Decimal2Literal at 0x7f6218e840d0>, <class 'datetime.date'>: <function Thing2Literal at 0x7f6218e84040>, <class 'datetime.datetime'>: <function DateTime2literal at 0x7f6218e5b6d0>, <class 'datetime.timedelta'>: <function DateTimeDelta2literal at 0x7f6218e5b760>, <class 'set'>: <function Set2Str at 0x7f6218e5bd90>, <class 'NoneType'>: <function None2NULL at 0x7f6218e5bf40>, <class 'int'>: <function Thing2Str at 0x7f6218e5be20>, <class 'float'>: <function Float2Str at 0x7f6218e5beb0>, <class 'bool'>: <function Bool2Str at 0x7f6218e5bc70>}, 'database': 'test-db', 'password': 'test_password', 'unix_socket': '/example/test-db', 'user': 'example_user'}
Would be better if all db credentials in debug mode should be masked also with '*'.
Attachments (1)
Change History (12)
by , 4 weeks ago
Attachment: | 2024-11-22_21-17.png added |
---|
follow-up: 3 comment:1 by , 4 weeks ago
Component: | Uncategorized → Error reporting |
---|---|
Resolution: | → needsinfo |
Status: | new → closed |
Type: | Uncategorized → Bug |
comment:2 by , 4 weeks ago
Description: | modified (diff) |
---|---|
Resolution: | needsinfo |
Status: | closed → new |
Summary: | Database password visible on debug page (view source only) → Database password visible on debug page |
Version: | 4.1 |
comment:3 by , 4 weeks ago
Replying to Tim Graham:
It's unclear how to reproduce the problem. Please reopen if you can provide a minimal example.
My bad, I just edited the ticket.
comment:4 by , 4 weeks ago
Summary: | Database password visible on debug page → Database password visible on debug page in local variable |
---|
Thanks, that was enough for me to reproduce the issue. I can see how this violates least surprise if these are substantially the same settings already masked elsewhere in the debug view.
The initial wontfix (comment:5:ticket:21098) in a related ticket for masking sensitive POST parameters argued masking wouldn't be worthwhile (leaks developer's own secrets to developer, DEBUG page is documented as always potentially leaking information), but ticket:21098 was eventually fixed once there was a simpler implementation.
Here is a potential tiny patch that I just confirmed fixes the issue, although it would be using @sensitive_variables()
outside the context of a view, which I took to be the use case it was designed for:
-
django/db/backends/base/base.py
diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index e6e0325d07..b950c20350 100644
a b from django.db.transaction import TransactionManagementError 20 20 from django.db.utils import DatabaseErrorWrapper, ProgrammingError 21 21 from django.utils.asyncio import async_unsafe 22 22 from django.utils.functional import cached_property 23 from django.views.decorators.debug import sensitive_variables 23 24 24 25 NO_DB_ALIAS = "__no_db__" 25 26 RAN_DB_VERSION_CHECK = set() … … class BaseDatabaseWrapper: 235 236 # ##### Backend-specific methods for creating connections ##### 236 237 237 238 @async_unsafe 239 @sensitive_variables("conn_params") 238 240 def connect(self): 239 241 """Connect to the database. Assume that the connection is closed.""" 240 242 # Check for invalid configurations. -
django/views/debug.py
diff --git a/django/views/debug.py b/django/views/debug.py index 10b4d22030..67e3f3d8b0 100644
a b class SafeExceptionReporterFilter: 234 234 if is_multivalue_dict: 235 235 # Cleanse MultiValueDicts (request.POST is the one we usually care about) 236 236 value = self.get_cleansed_multivaluedict(request, value) 237 else: 238 value = self.cleanse_setting("", value) 237 239 return value 238 240 239 241 def get_traceback_frame_variables(self, request, tb_frame):
comment:5 by , 4 weeks ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:6 by , 4 weeks ago
Resolution: | → needsinfo |
---|---|
Status: | assigned → closed |
Version: | → dev |
Thank you Jacob for the extra details, though I'm not being able to reproduce. Making connect
fail does not help me get a traceback at runtime, it only prevents the development server to run.
Can someone please provide, ideally, a regression test? Or at least a code snippet for a view to make this more easily reproducible.
Side note: if we eventually accept this ticket, I think it should include some audit of other potential places where the connection params could be used and not marked as sensitive.
follow-up: 11 comment:7 by , 4 weeks ago
Yeah, the failure has to be "occasional" enough to not prevent the development server from booting. I know this is lazier than writing a unit test, but two quick reproducers:
- Start your db, boot the development server, stop the db, and visit another page that makes a db query.
- Or, while keeping your db up, fiddle with simulating a "random" failure like this:
-
django/db/backends/postgresql/base.py
diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index c864cab57a..8f653f0078 100644
a b class DatabaseWrapper(BaseDatabaseWrapper): 181 181 _named_cursor_idx = 0 182 182 _connection_pools = {} 183 183 184 # Simulate an occasional failure. 185 denominator_will_become_zero = 5 186 184 187 @property 185 188 def pool(self): 186 189 pool_options = self.settings_dict["OPTIONS"].get("pool") … … class DatabaseWrapper(BaseDatabaseWrapper): 308 311 # default when no value is explicitly specified in options. 309 312 # - before calling _set_autocommit() because if autocommit is on, that 310 313 # will set connection.isolation_level to ISOLATION_LEVEL_AUTOCOMMIT. 314 self.denominator_will_become_zero -= 1 315 # Simulate occasional failure. 316 1 / self.denominator_will_become_zero 317 print(self.denominator_will_become_zero) 318 311 319 options = self.settings_dict["OPTIONS"] 312 320 set_isolation_level = False 313 321 try:
comment:8 by , 4 weeks ago
Resolution: | needsinfo |
---|---|
Status: | closed → new |
comment:9 by , 4 weeks ago
I think there's a deeper problem here.
We can have a lot of cases where we can leak classified data.
For example, using salted_hmac from crypto
If we specify wrong algorithm we can leak the secret key of django, I understand that this is because of wrong usage but I would like to hide them.
comment:10 by , 4 weeks ago
you can try to make something like SafeExceptionReporterFilter for local vars
but I am concerned about the performance of such a solution
comment:11 by , 4 weeks ago
Triage Stage: | Unreviewed → Accepted |
---|---|
Type: | Bug → Cleanup/optimization |
Replying to Jacob Walls:
Yeah, the failure has to be "occasional" enough to not prevent the development server from booting. I know this is lazier than writing a unit test, but two quick reproducers:
- Start your db, boot the development server, stop the db, and visit another page that makes a db query.
- Or, while keeping your db up, fiddle with simulating a "random" failure like this:
Thank you Jacob, I assumed so and I did try both of these before, but the bit that I was missing was to actually "go" to the DB to fetch or write something. In retrospect, this is obvious, but somehow I missed it. I managed to reproduce the issue, but as anticipated, I fear that this ticket, if accepted, has a scope beyond handling only connect
. Specifically, if accepted, we should at least:
- mark as sensitive every location that uses the connection params via calling
get_connection_params
- mark as sensitive
settings_dict
andconn_params
inget_connection_params
- audit the rest of the code for other parts using or accessing the DB settings/password in any way
I'm not convinced this is within the scope of what Django core should handle. It has always been stated that error report filtering is a best-effort solution and further measures should be taken to sanitize or guard the error logs. Despite my reservations, I'm inclined to accept this, as it could provide a clear improvement by carefully adding the sensitive_variables
decorator to the audited methods and functions.
Regarding Baha's comments, I think we should limit this ticket to "DB connection params" and avoid expanding the scope, as it’s better to focus on one specific issue. In summary, I'm accepting with the rationale/caveats expressed above.
It's unclear how to reproduce the problem. Please reopen if you can provide a minimal example.