diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index dc569b1..ff25baf 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1145,6 +1145,31 @@ function _drupal_trigger_error_with_delayed_logging($error_msg, $error_type = E_ } /** + * Invoke trigger_error() using a fatal error that will terminate the request. + * + * Normally, Drupal's error handler does not terminate script execution on + * user-level errors, even if the error is of type E_USER_ERROR. This function + * triggers an error of type E_USER_ERROR that is explicitly forced to be a + * fatal error which terminates script execution. + * + * @param string $error_msg + * The error message to trigger. As with trigger_error() itself, this is + * limited to 1024 bytes; additional characters beyond that will be removed. + * + * @see _drupal_error_handler_real() + */ +function drupal_trigger_fatal_error($error_msg) { + $fatal_error = &drupal_static(__FUNCTION__, FALSE); + $fatal_error = TRUE; + trigger_error($error_msg, E_USER_ERROR); + $fatal_error = FALSE; + // The standard Drupal error handler should have treated this as a fatal + // error and already ended the page request. But in case another error + // handler is being used, terminate execution explicitly here also. + exit; +} + +/** * Writes the file scan cache to the persistent cache. * * This cache stores all files marked as missing or moved after a file scan diff --git a/includes/database/query.inc b/includes/database/query.inc index 0c86287..334e805 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -871,8 +871,14 @@ class DeleteQuery extends Query implements QueryConditionInterface { $query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; if (count($this->condition)) { - - $this->condition->compile($this->connection, $this); + try { + $this->condition->compile($this->connection, $this); + } + // PHP does not allow exceptions to be thrown in __toString(), so trigger + // a fatal error instead. + catch (InvalidQueryConditionOperatorException $e) { + drupal_trigger_fatal_error($e->getMessage()); + } $query .= "\nWHERE " . $this->condition; } @@ -1204,7 +1210,14 @@ class UpdateQuery extends Query implements QueryConditionInterface { $query = $comments . 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields); if (count($this->condition)) { - $this->condition->compile($this->connection, $this); + try { + $this->condition->compile($this->connection, $this); + } + // PHP does not allow exceptions to be thrown in __toString(), so trigger + // a fatal error instead. + catch (InvalidQueryConditionOperatorException $e) { + drupal_trigger_fatal_error($e->getMessage()); + } // There is an implicit string cast on $this->condition. $query .= "\nWHERE " . $this->condition; } @@ -1821,8 +1834,8 @@ class DatabaseCondition implements QueryConditionInterface, Countable { $arguments += $condition['field']->arguments(); } else { - // If something passed in an invalid character, throw exception and - // stop early, so we don't rely on a broken SQL statement. + // If the operator contains an invalid character, throw an + // exception to protect against SQL injection attempts. if (stripos($condition['operator'], 'UNION') !== FALSE || strpbrk($condition['operator'], '[-\'"();') !== FALSE) { throw new InvalidQueryConditionOperatorException('Invalid characters in query operator: ' . $condition['operator']); } diff --git a/includes/database/select.inc b/includes/database/select.inc index 8d84460..9fa9603 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -1504,7 +1504,14 @@ class SelectQuery extends Query implements SelectQueryInterface { // the query will be executed, it will be recompiled using the proper // placeholder generator anyway. if (!$this->compiled()) { - $this->compile($this->connection, $this); + try { + $this->compile($this->connection, $this); + } + // PHP does not allow exceptions to be thrown in __toString(), so trigger + // a fatal error instead. + catch (InvalidQueryConditionOperatorException $e) { + drupal_trigger_fatal_error($e->getMessage()); + } } // Create a sanitized comment string to prepend to the query. diff --git a/includes/errors.inc b/includes/errors.inc index 3548d1f..4465f8f 100644 --- a/includes/errors.inc +++ b/includes/errors.inc @@ -62,7 +62,8 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c require_once DRUPAL_ROOT . '/includes/common.inc'; } - // We treat recoverable errors as fatal. + // We treat recoverable errors as fatal, and also allow fatal errors to be + // explicitly triggered by drupal_trigger_fatal_error(). _drupal_log_error(array( '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error', // The standard PHP error handler considers that the error messages @@ -72,7 +73,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c '%file' => $caller['file'], '%line' => $caller['line'], 'severity_level' => $severity_level, - ), $error_level == E_RECOVERABLE_ERROR); + ), $error_level == E_RECOVERABLE_ERROR || drupal_static('drupal_trigger_fatal_error')); } } @@ -269,7 +270,7 @@ function _drupal_log_error($error, $fatal = FALSE) { function _drupal_get_last_caller($backtrace) { // Errors that occur inside PHP internal functions do not generate // information about file and line. Ignore black listed functions. - $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); + $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler', 'drupal_trigger_fatal_error'); while (($backtrace && !isset($backtrace[0]['line'])) || (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { array_shift($backtrace);