Browse Source

Sanitize database error messages

Route DataAccessException and PersistenceException (including bare
ConstraintViolationException) to a unified handler that extracts the
constraint name and returns "Constraint violation: <name>" instead of
the raw PSQLException message. Other DB errors continue to return the
generic "Database error" response.

Adds DaoUtil.extractConstraintViolation helper and an integration test
that verifies no SQL details leak when an FK constraint is violated.
pull/15262/head
Viacheslav Klimov 5 months ago
parent
commit
688a1d52b3
Failed to extract signature
  1. 19
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  2. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  3. 21
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  4. 10
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

19
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.exception;
import jakarta.persistence.PersistenceException;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
@ -154,8 +156,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleAuthenticationException((AuthenticationException) exception, response);
} else if (exception instanceof MaxPayloadSizeExceededException) {
handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception);
} else if (exception instanceof DataAccessException e) {
handleDatabaseException(e, response);
} else if (exception instanceof DataAccessException || exception instanceof PersistenceException) {
handleDatabaseException(exception, response);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
@ -209,8 +211,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
ThingsboardErrorResponse errorResponse;
if (databaseException instanceof ConstraintViolationException) {
errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
ConstraintViolationException constraintViolationException = DaoUtil.extractConstraintViolation(databaseException);
if (constraintViolationException != null) {
log.debug("Constraint violation: {}", ExceptionUtils.getRootCauseMessage(databaseException));
String constraintName = constraintViolationException.getConstraintName();
String userMessage;
if (constraintName != null && !constraintName.isEmpty()) {
userMessage = "Constraint violation: " + constraintName;
} else {
userMessage = "Constraint violation";
}
errorResponse = ThingsboardErrorResponse.of(userMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
} else {
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);

2
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -1061,7 +1061,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
assertThat(findRelationsByTo(entityTo)).hasSize(1);
doDelete(urlDelete)
.andExpect(status().isInternalServerError());
.andExpect(status().isBadRequest());
assertThat(findRelationsByTo(entityTo)).hasSize(1);
} finally {

21
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -868,6 +869,26 @@ public class TenantControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService);
}
@Test
public void testSaveTenantWithNonExistentTenantProfileId() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
tenant.setTenantProfileId(new TenantProfileId(UUID.randomUUID()));
String responseBody = doPost("/api/tenant", tenant)
.andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
// Verify sanitized message format
assertThat(responseBody).contains("Constraint violation: fk_tenant_profile");
// Verify raw SQL details are not returned
assertThat(responseBody).doesNotContain("could not execute statement");
assertThat(responseBody).doesNotContain("insert or update on table");
assertThat(responseBody).doesNotContain("tenant_profile_id");
assertThat(responseBody).doesNotContain("is not present in table");
}
private void testBroadcastEntityStateChangeEventNeverTenant() {
Mockito.verify(tbClusterService, never()).onTenantChange(Mockito.any(Tenant.class),
Mockito.isNull());

10
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@ -202,4 +203,13 @@ public final class DaoUtil {
.collect(Collectors.toList());
}
public static ConstraintViolationException extractConstraintViolation(Throwable t) {
if (t instanceof ConstraintViolationException cve) {
return cve;
} else if (t != null && t.getCause() instanceof ConstraintViolationException cve) {
return cve;
}
return null;
}
}

Loading…
Cancel
Save