From 5ec41b1001a03c5f8c85ab0c5ec1bc9676b526ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:42:40 +0900 Subject: [PATCH 1/2] test(privacy): reproduce ETL resource identifier log leak --- .../EtlApiProblemHandlerLoggingTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/controller/EtlApiProblemHandlerLoggingTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlApiProblemHandlerLoggingTest.java b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlApiProblemHandlerLoggingTest.java new file mode 100644 index 00000000..2074d590 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlApiProblemHandlerLoggingTest.java @@ -0,0 +1,38 @@ +package com.xtrmetl.etl.controller; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that ordinary ETL failure logs retain bounded classifications without resource IDs. + */ +@ExtendWith(OutputCaptureExtension.class) +class EtlApiProblemHandlerLoggingTest { + + private static final String JOB_RECORD_ID = "0198f4cf-41c8-7f52-9e5d-private-job-marker"; + + @Test + void targetFailureLogDoesNotRepublishOwnerScopedResourceIdentifier(CapturedOutput output) { + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", + "/api/etl/jobs/" + JOB_RECORD_ID + ); + EtlApiProblemHandler handler = new EtlApiProblemHandler(); + + handler.handleTargetFailure( + new DataIntegrityViolationException("synthetic database diagnostic"), + request + ); + + String logs = output.getOut() + output.getErr(); + assertTrue(logs.contains("ETL target failure")); + assertFalse(logs.contains(JOB_RECORD_ID)); + } +} From 64d186351704ab6993343a5a8dc9b16f73501029 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:44:02 +0900 Subject: [PATCH 2/2] fix(privacy): keep ETL resource identifiers out of failure logs --- .../etl/controller/EtlApiProblemHandler.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlApiProblemHandler.java b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlApiProblemHandler.java index 6ab4ff3e..81a0e489 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlApiProblemHandler.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlApiProblemHandler.java @@ -24,7 +24,8 @@ *

The advice is scoped to the synchronous and durable-job ETL controllers; unrelated * controllers retain their existing exception contracts. Every client-visible field is fixed by a * typed definition. Raw exception messages, SQL details, credentials, paths, causes, and stack - * traces are never copied into the HTTP body. Covered responses use + * traces are never copied into the HTTP body. Ordinary failure logs keep bounded classifications + * without request resource paths or owner-scoped identifiers. Covered responses use * {@code Cache-Control: no-store} so authenticated operational failures are not retained by shared * or private caches. Spring MVC routing and response-negotiation failures are deliberately not * captured by a broad exception handler and therefore retain their framework-owned status @@ -58,11 +59,7 @@ public ResponseEntity handleRequestFailure( HttpServletRequest request ) { EtlRequestError error = exception.error(); - log.debug( - "Rejected ETL request path={} code={}", - request.getRequestURI(), - error.errorCode() - ); + log.debug("Rejected ETL request code={}", error.errorCode()); return problem( error.status(), error.type(), @@ -86,8 +83,7 @@ public ResponseEntity handleUnreadableBody( HttpServletRequest request ) { log.debug( - "Rejected unreadable ETL request path={} exceptionType={}", - request.getRequestURI(), + "Rejected unreadable ETL request exceptionType={}", exception.getClass().getName() ); EtlRequestError error = EtlRequestError.INVALID_JSON; @@ -114,8 +110,7 @@ public ResponseEntity handleTransientTargetFailure( HttpServletRequest request ) { log.warn( - "Transient ETL target failure path={} exceptionType={}", - request.getRequestURI(), + "Transient ETL target failure exceptionType={}", exception.getClass().getName() ); return problem( @@ -141,8 +136,7 @@ public ResponseEntity handleTargetFailure( HttpServletRequest request ) { log.error( - "ETL target failure path={} exceptionType={}", - request.getRequestURI(), + "ETL target failure exceptionType={}", exception.getClass().getName() ); return problem( @@ -168,8 +162,7 @@ public ResponseEntity handleUnexpectedFailure( HttpServletRequest request ) { log.error( - "Unexpected ETL failure path={} exceptionType={}", - request.getRequestURI(), + "Unexpected ETL failure exceptionType={}", exception.getCause().getClass().getName() ); return problem(