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 a6804a4
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/_posts/2024-xx-xx-v4.11.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`.
* Fix type of `purl` fields in Swagger docs - [apiserver/#3512]
* Fix CI build status badge - [apiserver/#3513]
* Fix `bom` and `vex` request fields not being visible in OpenAPI spec - [apiserver/#3557]
* Fix unclear error response when base64 encoded `bom` and `vex` values exceed character limit - [apiserver/#3558]
* Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682]
* Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686]
* Fix finding search routes - [frontend/#689]
Expand Down Expand Up @@ -180,6 +181,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi
[apiserver/#3513]: https://github.com/DependencyTrack/dependency-track/pull/3513
[apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522
[apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557
[apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558

[frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682
[frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683
Expand Down
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 VEX files, 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,86 @@
/*
* 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");
problemDetails.setDetail(createDetail(exception));

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

private static String createDetail(final JsonMappingException exception) {
if (!(exception.getCause() instanceof StreamConstraintsException)) {
return exception.getMessage();
}

final JsonMappingException.Reference reference = exception.getPath().get(0);
if (Objects.equals(reference.getFrom(), BomSubmitRequest.class)
&& "bom".equals(reference.getFieldName())) {
return """
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(exception.getMessage());
} else if (Objects.equals(reference.getFrom(), VexSubmitRequest.class)
&& "vex".equals(reference.getFieldName())) {
return """
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(exception.getMessage());
}

return exception.getMessage();
}

}
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 a6804a4

Please sign in to comment.