Skip to content

Commit

Permalink
Provide meaningful error message for bom and vex exceeding Jackso…
Browse files Browse the repository at this point in the history
…n's character limit

Also document the limitation in the OpenAPI spec of the respective `PUT` methods that accept JSON payloads.

Fixes #3182

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Mar 17, 2024
1 parent 753924d commit d07e283
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ public Response exportComponentAsCycloneDx (
a response with problem details in RFC 9457 format will be returned. In this case,
the response's content type will be <code>application/problem+json</code>.
</p>
<p>
The maximum allowed length of the <code>bom</code> value is 20'000'000 characters.
When uploading large BOMs, the <code>POST</code> endpoint is preferred,
as it does not have this limit.
</p>
<p>Requires permission <strong>BOM_UPLOAD</strong></p>""",
response = BomUploadResponse.class,
nickname = "UploadBomBase64Encoded"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public Response exportProjectAsCycloneDx (
a response with problem details in RFC 9457 format will be returned. In this case,
the response's content type will be <code>application/problem+json</code>.
</p>
<p>
The maximum allowed length of the <code>vex</code> value is 20'000'000 characters.
When uploading large BOMs, the <code>POST</code> endpoint is preferred,
as it does not have this limit.
</p>
<p>Requires permission <strong>VULNERABILITY_ANALYSIS</strong></p>"""
)
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.resources.v1.exception;

import com.fasterxml.jackson.core.exc.StreamConstraintsException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.dependencytrack.resources.v1.problems.ProblemDetails;
import org.dependencytrack.resources.v1.vo.BomSubmitRequest;
import org.dependencytrack.resources.v1.vo.VexSubmitRequest;

import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.Objects;

/**
* @since 4.11.0
*/
@Provider
@Priority(1)
public class JsonMappingExceptionMapper implements ExceptionMapper<JsonMappingException> {

@Context
private HttpServletRequest request;

@Context
private ResourceInfo resourceInfo;

@Override
public Response toResponse(final JsonMappingException exception) {
final var problemDetails = new ProblemDetails();
problemDetails.setStatus(400);
problemDetails.setTitle("The provided JSON payload could not be mapped");

String detail = exception.getMessage();
if (exception.getCause() instanceof StreamConstraintsException) {
if (exception.getPath() != null && !exception.getPath().isEmpty()) {
final JsonMappingException.Reference reference = exception.getPath().get(0);
if (Objects.equals(reference.getFrom(), BomSubmitRequest.class)
&& "bom".equals(reference.getFieldName())) {
detail = """
The BOM is too large to be transmitted safely via Base64 encoded JSON value. \
Please use the "POST /api/v1/bom" endpoint with Content-Type "multipart/form-data" instead. \
Original cause: %s""".formatted(detail);
} else if (Objects.equals(reference.getFrom(), VexSubmitRequest.class)

Check warning on line 65 in src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java#L65

Deeply nested if..then statements are hard to read
&& "vex".equals(reference.getFieldName())) {
detail = """
The VEX is too large to be transmitted safely via Base64 encoded JSON value. \
Please use the "POST /api/v1/vex" endpoint with Content-Type "multipart/form-data" instead. \
Original cause: %s""".formatted(detail);
}
}
}

problemDetails.setDetail(detail);

return Response
.status(Response.Status.BAD_REQUEST)
.type(ProblemDetails.MEDIA_TYPE_JSON)
.entity(problemDetails)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import alpine.common.util.UuidUtil;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import com.fasterxml.jackson.core.StreamReadConstraints;
import org.apache.http.HttpStatus;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
Expand All @@ -35,6 +36,7 @@
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.parser.cyclonedx.CycloneDxValidator;
import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper;
import org.dependencytrack.resources.v1.vo.BomSubmitRequest;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
Expand Down Expand Up @@ -68,7 +70,8 @@ protected DeploymentContext configureDeployment() {
new ResourceConfig(BomResource.class)
.register(ApiFilter.class)
.register(AuthenticationFilter.class)
.register(MultiPartFeature.class)))
.register(MultiPartFeature.class)
.register(JsonMappingExceptionMapper.class)))
.build();
}

Expand Down Expand Up @@ -930,4 +933,35 @@ public void uploadBomInvalidXmlTest() {
""");
}

@Test
public void uploadBomTooLargeViaPutTest() {
initializeWithPermissions(Permissions.BOM_UPLOAD);

final var project = new Project();
project.setName("acme-app");
project.setVersion("1.0.0");
qm.persist(project);

final String bom = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1);

final Response response = target(V1_BOM).request()
.header(X_API_KEY, apiKey)
.put(Entity.entity("""
{
"projectName": "acme-app",
"projectVersion": "1.0.0",
"bom": "%s"
}
""".formatted(bom), MediaType.APPLICATION_JSON));
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json");
assertThatJson(getPlainTextBody(response)).isEqualTo("""
{
"status": 400,
"title": "The provided JSON payload could not be mapped",
"detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])"
}
""");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import com.fasterxml.jackson.core.StreamReadConstraints;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.AnalysisResponse;
Expand All @@ -30,6 +31,7 @@
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.parser.cyclonedx.CycloneDxValidator;
import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
Expand All @@ -41,7 +43,6 @@
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import java.util.Base64;

import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
Expand All @@ -57,7 +58,8 @@ protected DeploymentContext configureDeployment() {
new ResourceConfig(VexResource.class)
.register(ApiFilter.class)
.register(AuthenticationFilter.class)
.register(MultiPartFeature.class)))
.register(MultiPartFeature.class)
.register(JsonMappingExceptionMapper.class)))
.build();
}

Expand Down Expand Up @@ -306,4 +308,33 @@ public void uploadVexInvalidXmlTest() {
""");
}

@Test
public void uploadVexTooLargeViaPutTest() {
final var project = new Project();
project.setName("acme-app");
project.setVersion("1.0.0");
qm.persist(project);

final String vex = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1);

final Response response = target(V1_VEX).request()
.header(X_API_KEY, apiKey)
.put(Entity.entity("""
{
"projectName": "acme-app",
"projectVersion": "1.0.0",
"vex": "%s"
}
""".formatted(vex), MediaType.APPLICATION_JSON));
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json");
assertThatJson(getPlainTextBody(response)).isEqualTo("""
{
"status": 400,
"title": "The provided JSON payload could not be mapped",
"detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])"
}
""");
}

}

0 comments on commit d07e283

Please sign in to comment.