View Issue Details

IDProjectCategoryView StatusLast Update
5341Composrcore_database_driverspublic2023-02-21 14:51
ReporterChris Graham Assigned ToGuest  
PrioritynormalSeverityfeature 
Status newResolutionopen 
Summary5341: protect_database_details option
DescriptionAdd a new installation option that, if set, hides details of database errors so they can only be found by looking into the stack trace (which is access-protected).
TagsHas Patch, Roadmap: Over the horizon, Type: Security
Attach Tags
Attached Files
protect_database_details.diff (21,749 bytes)   
diff --git a/config_editor.php b/config_editor.php
index 863f342a8f1ad5e3b95b8e9798b05606d194683b..4074d7e43e0e018c9a5d941ab90aae89a9739403 100644
--- a/config_editor.php
+++ b/config_editor.php
@@ -219,6 +219,7 @@ function do_access($given_password)
 
         'prefer_direct_code_call' => '<em>Tuning:</em> Whether to assume a good opcode cache is present, so load up full code files via this rather than trying to save RAM by loading up small parts of files on occasion.',
 
+        'protect_database_details' => '<em>Security:</em> Limit what information is disclosed when showing an error message relating to the database.',
         'backdoor_ip' => '<em>Security:</em> Always allow users accessing from this IP address in, automatically logged in as the oldest admin of the site.',
         'full_ips' => '<em>Security:</em> Whether to match sessions to the full IP addresses. Set this to 1 if you are sure users don\'t jump around IP addresses on the same 255.255.255.0 subnet (e.g. due to proxy server randomisation).',
         /*  Don't want this in here, we want it autodetected unless explicitly overridden
diff --git a/lang/EN/critical_error.ini b/lang/EN/critical_error.ini
index a963da830e9e8387445c68bf743df81b76ce7dbc..9546a88d1132d25e6714435aa74802b30e413648 100644
--- a/lang/EN/critical_error.ini
+++ b/lang/EN/critical_error.ini
@@ -166,13 +166,14 @@ ONLY_LOCAL_HOST_FOR_TYPE=This database driver only works over 'localhost'
 QUERY_FAILED=Unfortunately a query has failed [{1}] [<strong>{2}</strong>]
 QUERY_FAILED_TOO_BIG=Unfortunately a query of {2} bytes is too big for the current MySQL <kbd>max_allowed_packet</kbd> setting of {3} bytes [{1}]
 QUERY_NULL=A query that had to return something returned nothing: {1}
+QUERY_FAILED_GENERIC=A database operation has failed.
 MUST_DELETE_INSTALLER=You must delete the <kbd>install.php</kbd> file immediately, or your installation is open to erasure by anyone; if you installed via the official copy-to-hosting feature then you will be able to see and delete this file if you open up FTP manually
 ERROR_OCCURRED=An error has occurred
 ERROR_OCCURRED_SUBJECT=An error occurred on the '{1}' page
 SOME_ERRORS_OCCURRED=Some errors occurred during the action
 CORRUPT_FILE=A file, {1} is either corrupt or a filetype not recognised by this part of the website software
 STACK_TRACE=Here is the stack trace:
-STACK_TRACE_DENIED_ERROR_NOTIFICATION=The staff have been provided a &lsquo;stack trace&rsquo; which may help them diagnose the problem.
+STACK_TRACE_DENIED_ERROR_NOTIFICATION=The staff have been provided further information which may help them diagnose the problem.
 CONNECT_ERROR=Could not connect to the database with the database username you specified
 CONNECT_DB_ERROR=Could not connect to the database
 NO_PARAMETER_SENT=A critical parameter, <kbd>{1}</kbd>, was missing.
diff --git a/sources/database.php b/sources/database.php
index ddf97706b485ced6f7597660c7df9e3b7914e78b..a087a060915588706c891ca68cc39fd717c09e01 100644
--- a/sources/database.php
+++ b/sources/database.php
@@ -1147,7 +1147,7 @@ class DatabaseConnector
             return null; // error
         }
         if (!array_key_exists(0, $values)) {
-            fatal_exit(do_lang_tempcode('QUERY_NULL', escape_html($this->_get_where_expand($this->table_prefix . $table, array($selected_value), $where_map, $end)))); // No result found
+            fatal_exit_query(do_lang_tempcode('QUERY_NULL', escape_html($this->_get_where_expand($this->table_prefix . $table, array($selected_value), $where_map, $end)))); // No result found
         }
         return $this->_query_select_value($values);
     }
@@ -2000,3 +2000,64 @@ class DatabaseConnector
         return $locked;
     }
 }
+
+/**
+ * Equivalent to fatal_exit, but will substitute a vague error message based on configuration to avoid information disclosure.
+ * Real error message would be preserved in a stack trace.
+ *
+ * @param  mixed $text The error message (string or Tempcode)
+ */
+function fatal_exit_query($text)
+{
+    global $SITE_INFO;
+    if ((isset($SITE_INFO['protect_database_details'])) && ($SITE_INFO['protect_database_details'] == '1') && (!running_script('upgrader')) && (!running_script('execute_temp'))) {
+        if (!function_exists('do_lang') || (do_lang('QUERY_FAILED_GENERIC', null, null, null, null, false) === null)) {
+            $new_text = 'A database operation has failed.';
+        } else {
+            $new_text = do_lang('QUERY_FAILED_GENERIC');
+        }
+    } else {
+        $new_text = $text;
+    }
+    fatal_exit($new_text);
+}
+
+/**
+ * Equivalent to attach_message, but will substitute a vague error message based on configuration to avoid information disclosure.
+ * Real error message would be preserved in a stack trace if triggered.
+ *
+ * @sets_output_state
+ *
+ * @param  mixed $message The message
+ */
+function attach_message_query($message)
+{
+    global $SITE_INFO;
+    if ((isset($SITE_INFO['protect_database_details'])) && ($SITE_INFO['protect_database_details'] == '1') && (!running_script('upgrader')) && (!running_script('execute_temp'))) {
+        if (!function_exists('do_lang') || (do_lang('QUERY_FAILED_GENERIC', null, null, null, null, false) === null)) {
+            $message = 'A database operation has failed.';
+        } else {
+            $message = do_lang('QUERY_FAILED_GENERIC');
+        }
+    }
+    attach_message($message, 'warn');
+}
+
+/**
+ * Outputs a message, but will substitute a vague error message based on configuration to avoid information disclosure.
+ * Real error message would be preserved in a stack trace if triggered.
+ *
+ * @param  mixed $message The message
+ */
+function echo_message_query($message)
+{
+    global $SITE_INFO;
+    if ((isset($SITE_INFO['protect_database_details'])) && ($SITE_INFO['protect_database_details'] == '1') && (!running_script('upgrader')) && (!running_script('execute_temp'))) {
+        if (!function_exists('do_lang') || (do_lang('QUERY_FAILED_GENERIC', null, null, null, null, false) === null)) {
+            $message = 'A database operation has failed.';
+        } else {
+            $message = do_lang('QUERY_FAILED_GENERIC');
+        }
+    }
+    echo $message . "<br />\n";
+}
diff --git a/sources/database/access.php b/sources/database/access.php
index 8e47c0a1d1a65daeab304f672691717676501c57..744f1b50bd1232d9e08cad2b69336fdd56f9b40f 100644
--- a/sources/database/access.php
+++ b/sources/database/access.php
@@ -374,12 +374,12 @@ class Database_Static_access
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/ibm.php b/sources/database/ibm.php
index 4782ff817642aac08309c7096fc815bf6424eda7..a39c378aa82027943121fdba031dc1319c3959f1 100644
--- a/sources/database/ibm.php
+++ b/sources/database/ibm.php
@@ -358,12 +358,12 @@ class Database_Static_ibm
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/mysql.php b/sources/database/mysql.php
index 1eb490eb4d400cdbe2aabd0ce6fe46fd8e920cba..39d816b000c4010cfef7293c0a79c30fd6de5b38 100644
--- a/sources/database/mysql.php
+++ b/sources/database/mysql.php
@@ -212,9 +212,9 @@ class Database_Static_mysql extends Database_super_mysql
                 /*@mysql_query('SET max_allowed_packet=' . strval(intval(strlen($query) * 1.3)), $db); Does not work well, as MySQL server has gone away error will likely just happen instead */
 
                 if ($get_insert_id) {
-                    fatal_exit(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
+                    fatal_exit_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 } else {
-                    attach_message(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))), 'warn');
+                    attach_message_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 }
                 return null;
             }
@@ -260,11 +260,11 @@ class Database_Static_mysql extends Database_super_mysql
                 }
 
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/mysql_dbx.php b/sources/database/mysql_dbx.php
index 1a589d8f59bb7bbb3cfb0ec168b02e2de4a8956b..1a3241fcbec0ba079ef786fdae182f661f520779 100644
--- a/sources/database/mysql_dbx.php
+++ b/sources/database/mysql_dbx.php
@@ -182,9 +182,9 @@ class Database_Static_mysql_dbx extends Database_super_mysql
                 /*@mysql_query('SET max_allowed_packet=' . strval(intval(strlen($query) * 1.3)), $db); Does not work well, as MySQL server has gone away error will likely just happen instead */
 
                 if ($get_insert_id) {
-                    fatal_exit(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
+                    fatal_exit_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 } else {
-                    attach_message(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))), 'warn');
+                    attach_message_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 }
                 return null;
             }
@@ -211,11 +211,11 @@ class Database_Static_mysql_dbx extends Database_super_mysql
                 }
 
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/mysqli.php b/sources/database/mysqli.php
index 6243335ac8fc7db728495b673b984af7322582ac..03f9ca11afd020769b8862f0795505db9c733a4d 100644
--- a/sources/database/mysqli.php
+++ b/sources/database/mysqli.php
@@ -213,9 +213,9 @@ class Database_Static_mysqli extends Database_super_mysql
                 /*@mysql_query('SET max_allowed_packet=' . strval(intval(strlen($query) * 1.3)), $db); Does not work well, as MySQL server has gone away error will likely just happen instead */
 
                 if ($get_insert_id) {
-                    fatal_exit(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
+                    fatal_exit_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 } else {
-                    attach_message(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))), 'warn');
+                    attach_message_query(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                 }
                 return null;
             }
@@ -265,11 +265,11 @@ class Database_Static_mysqli extends Database_super_mysql
                 }
 
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/oracle.php b/sources/database/oracle.php
index 9ec3ceef2fd14b2f83aa7e4701f784d772acf874..e5a9115e8bcbf76778e9875d5fe3996c7aae04d0 100644
--- a/sources/database/oracle.php
+++ b/sources/database/oracle.php
@@ -468,12 +468,12 @@ class Database_Static_oracle
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
@@ -529,7 +529,7 @@ class Database_Static_oracle
                         $v = $v->load(); // For CLOB's
                     }
                     if ($v === false) {
-                        fatal_exit(do_lang_tempcode('QUERY_FAILED', ocierror($stmt)));
+                        fatal_exit_query(do_lang_tempcode('QUERY_FAILED', ocierror($stmt)));
                     }
 
                     $name = $names[$j];
diff --git a/sources/database/postgresql.php b/sources/database/postgresql.php
index 41d80c1e2e9b46baaf96a5d006cda0fab5a154c7..1a2d9b06d90d9115ed3b411dcd7e603a5e9d515f 100644
--- a/sources/database/postgresql.php
+++ b/sources/database/postgresql.php
@@ -464,12 +464,12 @@ class Database_Static_postgresql
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/sqlite.php b/sources/database/sqlite.php
index 768703b4f190e24a5c902005db32129e328f9bac..cb8f9dc00c9b63fd3593a13051a16dbc386f1f59 100644
--- a/sources/database/sqlite.php
+++ b/sources/database/sqlite.php
@@ -371,12 +371,12 @@ class Database_Static_sqlite
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
diff --git a/sources/database/sqlserver.php b/sources/database/sqlserver.php
index 5019528e3b16937f9011d23d945693e81d03d71a..a4c660f116583c3770a99f4d88080d692fd11a8e 100644
--- a/sources/database/sqlserver.php
+++ b/sources/database/sqlserver.php
@@ -490,12 +490,12 @@ class Database_Static_sqlserver
             }
             if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1))) {
                 if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
-                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
+                    fatal_exit_query(htmlentities('Database query failed: ' . $query . ' : ' . $err));
                 }
 
-                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
+                fatal_exit_query(do_lang_tempcode('QUERY_FAILED', escape_html($query), $err));
             } else {
-                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
+                echo_message_query(htmlentities('Database query failed: ' . $query . ' [') . $err . htmlentities(']'));
                 return null;
             }
         }
protect_database_details.diff (21,749 bytes)   
Time estimation (hours)1.5
Sponsorship open

Sponsor

Date Added Member Amount Sponsored

Activities

PDStig

2023-02-21 02:55

administrator   ~7946

IMO this should be on by default because sensitive / private information could be leaked.

Chris Graham

2023-02-21 14:51

administrator   ~7950

Part of me agrees, but I am also concerned that it will degrade the quality of bug reports, leading to people reporting very generic database error messages. Perhaps my concern can be met by ensuring the error messages get a coordinating ID against what goes into the telemetry (assuming the user has that enabled). We'd also need to make sure that users of sites can report errors in clear way, because the error log doesn't have stack traces and I'm not sure if the admins will have error notifications turned on. Maybe we need to consider coordinating IDs in the error log and automatically linking to some directory of dumped stack traces. We already have a system whereby error messages can be replaced with a generic error screen, with actual traces logged into a directory - perhaps that needs merging with these ideas.

Add Note

View Status
Note
Upload Files
Maximum size: 32,768 KiB

Attach files by dragging & dropping, selecting or pasting them.
You are not logged in You are not logged in. This means you will not get any e-mail notifications. And if you reply, we will not know for sure you are the original poster of the issue.

Issue History

Date Modified Username Field Change
2023-02-21 02:24 Chris Graham New Issue
2023-02-21 02:24 Chris Graham Tag Attached: Has Patch
2023-02-21 02:24 Chris Graham Tag Attached: Roadmap: v12
2023-02-21 02:24 Chris Graham Tag Attached: Type: Security
2023-02-21 02:24 Chris Graham File Added: protect_database_details.diff
2023-02-21 02:24 Chris Graham Time estimation (hours) => 1.5
2023-02-21 02:55 PDStig Note Added: 0007946
2023-02-21 14:50 Guest Note Added: 0007949
2023-02-21 14:51 Chris Graham Note Added: 0007950
2024-03-26 00:58 PDStig Tag Renamed Roadmap: v12 => Roadmap: Over the horizon