adesso Blog

Seit Version 2.3 verbirgt das Spring Boot Framework standardmäßig genauere Details zu Fehlern, die beim Bearbeiten einer Anfrage auftreten. Dies betrifft unter anderem die Meldungen von Exceptions sowie die Meldungen, die in den Validierungs-Annotations angegeben werden. Häufig ist es in einem Projekt jedoch notwendig, Details über den Fehler im Frontend zur Verfügung zu haben.

Beispielsweise ist es keine gute User Experience, wenn bei einer 400-Bad-Request-Antwort vom Backend eine generische Fehlermeldung “Diese Anfrage konnte nicht erfolgreich verarbeitet werden” angezeigt wird. Hier sollte das Frontend anhand des Fehlers unterscheiden, ob der Fehler aufgetreten ist, weil der Registrierungslink abgelaufen ist oder weil der Benutzername bereits vergeben ist. Dazu benötigt es aber die entsprechenden Details zum Fehler.

In diesem Blog-Beitrag gehe ich darauf ein, warum Fehlerdetails sicherheitsrelevant sein können und wie man mit Spring Boot trotzdem die notwendigen Informationen zu den Nutzenden bringen kann.

Warum werden Fehlerdetails standardmäßig versteckt

Bis zur Spring Boot Version 2.3 wurden Details über den Fehler wie die message der Exception oder der Validierungs-Annotation immer in der Antwort mitgeliefert. Die Änderung hat das Spring-Boot-Projekt mit gutem Grund umgesetzt. Zuvor wurden diese Details bei allen Exceptions mit in die Fehlerantwort aufgenommen.

Bei Fehlern, die tiefer in der Anwendung auftreten, stellt das jedoch eine Gefahr für die Sicherheit dar. Denn eine detaillierte Fehlermeldung über eine fehlgeschlagene SQL-Abfrage oder einen fehlgeschlagenen Aufruf an ein Drittsystem kann von einem Angreifer beispielsweise genutzt werden, um Implementierungsdetails über die Anwendung oder sogar Daten von Kundinnen und Kunden zu extrahieren.

Die Lösung wäre nun, bei technischen oder unerwarteten Fehlern die Details zu verbergen und nur bei bestimmten technischen Fehlern genügend Informationen zu liefern, um die Fehlerfälle unterscheiden zu können.

Unsicheres aktivieren von Fehlermeldungen

Über die Konfigurationen server.error.include-message=never|on-param|always und server.error.include-binding-errors=never|on-param|always können diese Details auch heute noch wieder aktiviert werden. Sowohl der Wert always als auch on-param bergen jedoch die oben genannte Gefahr und sollten daher nicht verwendet werden.

always schreibt die Details immer in eine Fehlerantwort. on-param schreibt die Details in die Fehlerantwort, wenn die Anfrage den Parameter message=true enthält. Da die Anfrage aber auch vom Angreifenden kontrolliert wird, hilft das der Sicherheit nicht.

Mit aktiviertem include-message hat eine Fehlerantwort das folgende Format:

	
		{
		  "timestamp": "2024-08-07T19:07:36.173+00:00",
		  "status": 500,
		  "error": "Internal Server Error",
		  "message": "Malformed SQL query",
		  "path": "/"
		}
	

Ohne include-message bleibt dasselbe Format erhalten, allerdings ohne den Eintrag message . Die oben genannten Konfigurationen sollten also auf dem Standardwert never belassen werden.

Fachliche Exceptions definieren

Ein bewährter und von Spring gut unterstützter Ansatz ist es, für jeden fachlichem Fehlerfall, eine Exception-Klasse zu definieren. Diese Exception kann dann mit der Spring-Annotation @ResponseStatus versehen werden, um automatisch in eine Fehlerantwort mit dem entsprechende Http-Status umgewandelt zu werden, wenn sie auftritt.

	
		@ResponseStatus(value = HttpStatus.BAD_REQUEST,
		  reason = "DUPLICATE_USERNAME")
		public class DuplicateUsernameException extends RuntimeException {}
	

Der reason-Parameter ist der Parameter, der später als message in der Antwort erscheinen soll. Hier wird er auf den vom Frontend interpretierbaren Wert DUPLICATE_USERNAE gesetzt.

Wie kann die Fehlerantwort nun bei fachlichen Fehlern um die nötigen Details ergänzt werden?

Mögliche Lösung mit einem ControllerAdvice

Mit einem ControllerAdvice ControllerAdvice bietet Spring die Möglichkeit, die Antwort für ausgewählte Fehler selbst zu gestalten. Für den technischen Fehler, dass ein Benutzername bereits vergeben ist, könnte dies beispielsweise wie folgt aussehen:

	
		@ControllerAdvice
		public class DuplicateUsernameExceptionHandler
		    extends ResponseEntityExceptionHandler {
		  @ExceptionHandler(value = { DuplicateUsernameException.class })
		  protected ResponseEntity<CustomErrorModel> handleDuplicateUsername(
		      DuplicateUsernameException ex, WebRequest request) {
		    var errorModel = CustomErrorModel.forDuplicateUser(ex.getUsername());
		    return handleExceptionInternal(
		      ex, errorModel, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
		  }
		}
	

Dieser Ansatz erfordert jedoch, dass ein selbstdefiniertes Datenmodell für die Fehlerreaktion definiert wird. Fehler, die auf diese Weise behandelt werden, haben dann das selbst definierte Format, während alle anderen Fehler, die in der Anwendung auftreten können, weiterhin dem Standardformat von Spring entsprechen.

Der Ansatz mit einem ControllerAdvice ist daher nur dann geeignet, wenn die API es erfordert, dass für alle Fehlerantworten ein eigenes Format zurückgegeben wird. Dies ist in der Regel nur bei sehr großen Projekten oder extern angebotenen APIs notwendig.

Für viele Projekte wäre es besser, dem Standardformat von Spring zu folgen und trotzdem nur bei ausgewählten Fehlern erweiterte Details mit zurückzugeben.

Standardformat, aber trotzdem die Kontrolle?

Spring bietet mit der ErrorAttributes-Bean die Möglichkeit, für jede Anfrage, bei der ein Fehler aufgetreten ist, zu entscheiden, welche der verfügbaren Details zum Fehler in der Antwort enthalten sein sollen. Die Antwort behält das zuvor gezeigte Format, es wird nur ausgewählt, welche Einträge enthalten sein sollen.

Der Ansatz, den ich nun vorstelle, erfordert, wie der vorherige Ansatz, dass für jeden technischen Fehler eine eigene Exception-Klasse definiert wird. Ziel ist es, eine Möglichkeit zu schaffen, bestimmte technische Exceptions zu markieren, bei denen die Fehlernachricht in der Antwort enthalten sein soll.

Um das zu konfigurieren, könnt ihr die folgende ErrorAttributes-Bean im Spring-Kontext registrieren:

	
		@Bean
		public ErrorAttributes errorAttributes() {
		  // Die Standardimplementierung von ErrorAttributes verwenden ...
		  return new DefaultErrorAttributes() {
		    // ... aber eine kleine Änderung bei den ErrorAttributes vornehmen
		    @Override
		    public Map<String, Object> getErrorAttributes(
		        WebRequest webRequest, ErrorAttributeOptions options) {
		      final var error = getError(webRequest);
		      if (shouldIncludeErrorMessage(error)) {
		        options = options.including(Include.MESSAGE);
		      }
		      return super.getErrorAttributes(webRequest, options);
		    }
		  };
		}
		private static boolean shouldIncludeErrorMessage(Throwable error) {
		  // todo: decide when to include the exception message
		  return false;
		}
	

Da wir das Standardverhalten zum Großteil beibehalten möchten, verwenden wir eine Instanz der DefaultErrorAttributes und überschreiben nur die Methode, die entscheidet, welche Attribute enthalten sein sollen. In dieser Methode fragt man mit getError den Fehler ab, der während der Verarbeitung aufgetreten ist. Dann prüft man in shouldIncludeErrorMessage, ob der Fehler eine von uns angegebene Bedingung erfüllt. Wenn das der Fall ist, fügt man den Wert MESSAGE den ErrorAttributeOptions hinzu und gibt die angepassten ErrorAttributeOptions dann an die Standardimplementierung zurück.

Wir nehmen hier also nur einen minimalinvasiven Eingriff vor und lassen alles andere wie es ist. Aber mit diesem Eingriff haben wir einen guten Integrationspunkt in die Fehlerbehandlung, mit dem wir je nach aufgetretenem Fehler beliebig entscheiden können, welche Details in der Antwort enthalten sein sollen.

Nun müssen wir nur noch in shouldIncludeErrorMessage entscheiden, wie der Fehler behandelt werden soll. Eine Möglichkeit wäre es, in dieser Methode aufzulisten, bei welchen Fehlern sie greifen soll:

	
		private static boolean shouldIncludeErrorMessage(Throwable error) {
		  return error != null
		    && error.getClass() == DuplicateUsernameException.class;
		}
	

Mit dieser kleinen Konfiguration können wir beim Standardformat der Fehlerantwort bleiben, die Details standardmäßig ausblenden, sie aber trotzdem bei ausgewählten Fehlern zurückgeben. Besser wäre es, dieses Verhalten direkt in der Exception-Klasse definieren zu können.

Fachliche Fehler per Annotation markieren

In Java gibt es mehrere Möglichkeiten, Klassen so zu kennzeichnen, dass diese Kennzeichnung später im Programm abgefragt werden kann. Dies kann beispielsweise über eine gemeinsame Oberklasse oder die Implementierung eines Interfaces geschehen. Da die Klassenhierarchie der Exceptions aber auch für andere Zwecke verwendet werden kann, stelle ich hier eine orthogonale Lösung mit Annotationen vor.

Zuerst wird eine neue Annotation definiert, die die Exception-Klassen kennzeichnet, für die der Fehler in der Antwort enthalten sein soll:

	
		// Die Annotation gilt nur an Klassen, nicht an Feldern oder Methoden 
		@Target(ElementType.TYPE) 
		// Die Annotation wird zur Laufzeit der Anwendung benötigt
		@Retention(RetentionPolicy.RUNTIME)
		public @interface IncludeExceptionMessage {}
	

Dann können wir die zuvor definierte Methode shouldIncludeErrorMessage so anpassen, dass sie auf alle Exceptions mit dieser Annotation reagiert:

	
		private static boolean shouldIncludeErrorMessage(Throwable error) {
		  return error != null &&
		    error.getClass().isAnnotationPresent(IncludeExceptionMessage.class);
		}
	

Und zuletzt markieren wir unseren fachlichen Fehler mit der neuen Annotation:

	
		@IncludeExceptionMessage
		@ResponseStatus(value = HttpStatus.BAD_REQUEST,
		  reason = "DUPLICATE_USERNAME")
		public class DuplicateUsernameException extends RuntimeException {}
	

Jetzt können wir isoliert in einem beliebigen Teil der Anwendung neue Fehler definieren und müssen uns nicht darum kümmern eine zentrale Konfiguration anzupassen.

Ihr möchtet gern mehr über spannende Themen aus der adesso-Welt erfahren? Dann werft auch einen Blick in unsere bisher erschienenen Blog-Beiträge.

Bild Jannis Kaiser

Autor Jannis Kaiser

Jannis Kaiser ist Software Engineer bei adesso und seit vier Jahren im Bankenumfeld tätig. Sein Schwerpunkt liegt in der Entwicklung von Webanwendungen in Spring-Boot. Er unterstützt Kunden bei der Absicherung von Software.

Diese Seite speichern. Diese Seite entfernen.