jerryshao commented on code in PR #13057:
URL: https://github.com/apache/gravitino/pull/13057#discussion_r3976730040


##########
server-common/src/main/java/org/apache/gravitino/server/web/JettyServerConfig.java:
##########
@@ -112,6 +112,17 @@ public final class JettyServerConfig {
           .checkValue(value -> value > 0, 
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
           .createWithDefault(128 * 1024);
 
+  public static final ConfigEntry<Boolean> INCLUDE_ERROR_STACK_TRACE =
+      new ConfigBuilder("includeErrorStackTrace")
+          .doc(
+              "Whether to include server-side stack traces in HTTP error 
responses. Set this to "
+                  + "false in new deployments because stack traces can expose 
internal "
+                  + "implementation details. It remains true by default only 
to avoid breaking "
+                  + "legacy clients that expect the stack field")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .booleanConf()

Review Comment:
   **correctness (high) — scope gap, verified**: This config only governs the 
main Gravitino server's own `ErrorResponse` serialization path. Two other REST 
services shipped in this repository are completely untouched and still 
unconditionally leak full stack traces regardless of this setting:
   
   1. `iceberg/iceberg-rest-server/.../service/IcebergRESTUtils.java:428-437` — 
`errorResponse(Throwable, int)` (used by the globally-registered 
`IcebergExceptionMapper`) does 
`ErrorResponse.builder()....withStackTrace(ex).build()` unconditionally, on 
Iceberg's own `org.apache.iceberg.rest.responses.ErrorResponse`, with zero 
reference to `includeErrorStackTrace` anywhere in that file.
   2. `lance/lance-rest-server/.../service/LanceExceptionMapper.java:70-87,131` 
— every branch of `toResponse(Throwable)` embeds 
`org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(ex)` into the 
response's `detail` field, again completely independent of this flag.
   
   An operator who sets `includeErrorStackTrace=false` on the main server — 
following this PR's own recommendation — would reasonably believe the CWE-209 
exposure is closed, but any client hitting the Iceberg REST catalog endpoint or 
the Lance REST endpoint still gets a full server-side Java stack trace in every 
error response. Given the stated goal of this PR, I'd consider this a release 
blocker rather than a nice-to-have follow-up.
   
   ---
   
   **altitude**: Separately, this flag defaults to `true` (stack traces 
exposed) purely for legacy-client compatibility — the PR's own doc comment here 
admits this and tells operators to flip it to `false`. That's an opt-in-safety 
design: every fresh OSS install, Docker image, or Helm chart that doesn't 
explicitly override this ships CWE-209-exposed by default. A safer design would 
default to `false` with a documented legacy-compatibility opt-in, or move 
`stack` out of the response body into server-side-only diagnostics entirely.



##########
server-common/src/main/java/org/apache/gravitino/server/web/JettyServerConfig.java:
##########
@@ -112,6 +112,17 @@ public final class JettyServerConfig {
           .checkValue(value -> value > 0, 
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
           .createWithDefault(128 * 1024);
 
+  public static final ConfigEntry<Boolean> INCLUDE_ERROR_STACK_TRACE =

Review Comment:
   **conventions**: This new public field `INCLUDE_ERROR_STACK_TRACE` has no 
Javadoc comment — only a `.doc(...)` builder string, which documents the config 
value to operators, not the Java field/API itself. CLAUDE.md: "All new `public` 
and `protected` classes, methods, and fields must have Javadoc" (checkstyle 
enforces this with `-Werror` per this repo's CI).



##########
server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java:
##########
@@ -183,7 +206,7 @@ protected void sendAuthErrorResponse(HttpServletResponse 
response, Exception exc
     response.setStatus(httpStatus);
     response.setContentType("application/json");
     response.setCharacterEncoding(StandardCharsets.UTF_8.name());
-    ObjectMapperProvider.objectMapper().writeValue(response.getWriter(), 
errorResponse);
+    objectMapper.writeValue(response.getWriter(), errorResponse);

Review Comment:
   **correctness**: `includeErrorStackTrace` (via `objectMapper` here) only 
gates the `stack` field; it does nothing for `message`. Every branch of 
`sendAuthErrorResponse` above (lines 188-203) builds the response's `message` 
field directly from the raw, uncontrolled `exception.getMessage()` — including 
the catch-all `ErrorResponse.internalError(exception.getMessage(), exception)` 
at line 203 for unanticipated exceptions. For a bug in a custom 
`Authenticator`, an NPE, or a backend/token-store connection failure, 
`getMessage()`/the exception's own text commonly includes internal class names, 
file paths, or connection details — even with `includeErrorStackTrace=false`, 
that text still ships in the response body. Same pattern exists in 
`server/.../web/mapper/ErrorExceptionMapper.java:40-43` for uncaught 
`java.lang.Error`. Worth considering whether `message` needs the same treatment 
`stack` just got.
   
   ---
   
   **correctness**: Separately, `doFilter`'s catch blocks (lines 168-171, 
calling `sendAuthErrorResponse`) never log anything server-side — unlike the 
sibling authorization path (`GravitinoInterceptionService`) that this same PR 
updated to `LOG.warn(..., ex)`. Under the PR's own recommended config 
(`includeErrorStackTrace=false`), an unexpected exception during authentication 
is now both omitted from the client response *and* never logged server-side, 
losing all diagnostics for authentication-layer bugs — exactly the failure mode 
the PR description says it solved for the authorization path, just not carried 
over to this one.



##########
server-common/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java:
##########
@@ -28,38 +29,88 @@
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
+import org.apache.gravitino.dto.responses.ErrorResponse;
 
 @Provider
 public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
 
+  // Keep diagnostic stacks inside the server and accept legacy payloads while 
allowing operators
+  // to omit them from responses.
+  @JsonIgnoreProperties(value = "stack", allowSetters = true)
+  private abstract static class ErrorResponseMixin {}
+
   private static class ObjectMapperHolder {
-    private static final ObjectMapper INSTANCE =
-        JsonMapper.builder()
-            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
-            .configure(EnumFeature.WRITE_ENUMS_TO_LOWERCASE, true)
-            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
-            .build()
-            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
-            .registerModule(new JavaTimeModule())
-            .registerModule(new Jdk8Module());
+    private static final ObjectMapper WITHOUT_ERROR_STACK_TRACE = 
createObjectMapper(false);
+    private static final ObjectMapper WITH_ERROR_STACK_TRACE = 
createObjectMapper(true);
+  }
+
+  private final ObjectMapper objectMapper;
+
+  /**
+   * Creates a provider using the backward-compatible server default, which 
includes error stack
+   * traces.
+   */
+  public ObjectMapperProvider() {
+    this(JettyServerConfig.INCLUDE_ERROR_STACK_TRACE.getDefaultValue());
+  }
+
+  /**
+   * Creates a provider with explicit error stack-trace serialization behavior.
+   *
+   * @param includeErrorStackTrace whether HTTP error responses should include 
diagnostic stack
+   *     traces
+   */
+  public ObjectMapperProvider(boolean includeErrorStackTrace) {
+    this.objectMapper = objectMapper(includeErrorStackTrace);
   }
 
   /**
-   * Retrieves a globally shared {@link ObjectMapper} instance.
+   * Retrieves the shared {@link ObjectMapper} using the backward-compatible 
server default.
    *
-   * <p>Note: This ObjectMapper is a global single instance. If you need to 
modify the default
-   * serialization/deserialization settings, make changes within the INSTANCE 
builder directly.
-   * Avoid modifying properties of the returned {@code ObjectMapper} instance 
to prevent unintended
-   * side effects.
+   * <p>Do not modify the returned mapper. Use {@link #objectMapper(boolean)} 
to select explicit
+   * error stack-trace behavior.
    *
    * @return the globally shared {@link ObjectMapper} instance
    */
   public static ObjectMapper objectMapper() {
-    return ObjectMapperHolder.INSTANCE;
+    return 
objectMapper(JettyServerConfig.INCLUDE_ERROR_STACK_TRACE.getDefaultValue());

Review Comment:
   **reuse**: This "resolve the backward-compatible default" expression 
(`JettyServerConfig.INCLUDE_ERROR_STACK_TRACE.getDefaultValue()`) is 
independently duplicated here and in the constructor a few lines up (line 54), 
and again in `VersioningFilter`'s no-arg constructor 
(`server/.../VersioningFilter.java:~102`), instead of one delegating to a 
single shared resolution point — e.g. `VersioningFilter()` could just call 
`ObjectMapperProvider.objectMapper()` (the way `AuthenticationFilter()` already 
does) instead of re-deriving the same boolean and re-invoking the two-arg 
overload itself. If the default-resolution logic ever changes (env-var 
override, renamed config key), it's easy to update some call sites and miss 
others, leaving them silently serving stale default behavior.



##########
server-common/src/main/java/org/apache/gravitino/server/web/JettyServer.java:
##########
@@ -542,6 +542,6 @@ public void addSystemFilters(String pathSpec) {
    * custom authentication filter (e.g., one that returns Iceberg-spec JSON 
error responses).
    */
   protected Filter createAuthenticationFilter() {

Review Comment:
   **altitude**: `createAuthenticationFilter()` is a plain overridable method 
with nothing forcing an override to consult 
`serverConfig.isIncludeErrorStackTrace()`. Confirmed both Iceberg's 
`RESTService.java:96-97` and `LanceJettyServer.createAuthenticationFilter()` 
override this and always construct their own auth filter via a no-arg 
constructor, silently dropping the config flag (currently non-leaking only 
because neither `IcebergAuthenticationFilter` nor `LanceAuthenticationFilter`'s 
own DTO has a `stack` field — but that's incidental, not enforced).
   
   This demonstrates the "thread the boolean through every constructor" pattern 
this PR relies on isn't backed by any compiler check, test, or checkstyle rule 
— a future REST module added the same way (copying the existing Iceberg/Lance 
pattern) will silently reintroduce full stack-trace leakage in its auth-error 
path with nothing catching it.



##########
server-common/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java:
##########
@@ -28,38 +29,88 @@
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
+import org.apache.gravitino.dto.responses.ErrorResponse;
 
 @Provider
 public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
 
+  // Keep diagnostic stacks inside the server and accept legacy payloads while 
allowing operators
+  // to omit them from responses.
+  @JsonIgnoreProperties(value = "stack", allowSetters = true)

Review Comment:
   **efficiency**: The suppression mechanism is serialization-time only (this 
Jackson mixin), not suppression at the point the stack string is produced. 
`ErrorResponse`'s factory methods 
(`common/.../dto/responses/ErrorResponse.java`, e.g. 
`internalError`/`forbidden`/`unauthorized`) still call 
`getStackTrace(throwable)` unconditionally to build the field, and this mixin 
just discards the result when `includeErrorStackTrace=false`. So disabling the 
config saves response bytes but not the CPU/allocation cost of building a 
(potentially deep, multi-cause) stack-trace string on every failed request. 
Threading the boolean into `ErrorResponse`'s construction (skip `getStackTrace` 
entirely when disabled) would avoid the cost outright instead of just hiding 
the result.



##########
server-common/src/main/java/org/apache/gravitino/server/web/JettyServer.java:
##########
@@ -116,7 +116,7 @@ public synchronized void initialize(
 
     // Set error handler for Jetty Server
     ErrorHandler errorHandler = new ErrorHandler();
-    errorHandler.setShowStacks(true);
+    errorHandler.setShowStacks(serverConfig.isIncludeErrorStackTrace());

Review Comment:
   **test-coverage**: This new conditional 
(`errorHandler.setShowStacks(serverConfig.isIncludeErrorStackTrace())`, 
replacing a hardcoded `true`) has zero test coverage in either direction — 
`grep -rn "setShowStacks|showStacks" server-common/src/test server/src/test` 
returns no matches anywhere. This is the pre-Jersey, raw Jetty error path (e.g. 
malformed HTTP requests that never reach a Jersey resource); a regression here 
(inverted boolean, or the call silently dropped in a future refactor) would 
leak stack traces for a whole class of errors that bypass Jersey's exception 
mappers entirely, with nothing to catch it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to