POST /api/v1/apps returns 500 with a raw NPE when a secret's value is missing, instead of 400
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
POST /api/v1/apps/{key}/{siteId} returns HTTP 500 with a raw Java NPE message when a request body omits a secret's value, instead of a 400 with a useful validation error.
Input.InputDeserialize dereferences the value node without checking it, while null-guarding hidden on the very next line:
// dotCMS/src/main/java/com/dotcms/rest/api/v1/apps/Input.java:60-65
final JsonNode jsonNode = jsonParser.readValueAsTree();
final JsonNode value = jsonNode.get("value");
final JsonNode hidden = jsonNode.get("hidden");
return newInputParam(value.asText().trim().toCharArray(), // <-- NPE when "value" is absent
hidden != null && hidden.asBoolean()); // <-- correctly guarded
The asymmetry in adjacent lines suggests an oversight rather than a decision. Input even declares @NotNull on the field, but bean validation cannot help: the NPE happens during deserialization, before SecretForm.checkValid() ever runs.
Steps to Reproduce
SITE=<a site identifier>
AUTH=$(printf 'admin@dotcms.com:admin' | base64)
# Any body where an Input object has no "value" key. Here the caller wrapped the map in
# the field name, so Jackson maps "inputParams" -> Input, and that node has no "value".
curl -s -X POST -H "Authorization: Basic $AUTH" -H "Content-Type: application/json" \
-d '{"inputParams":{"apiKey":{"value":"anything","hidden":true}}}' \
"http://localhost:8082/api/v1/apps/dotGoogleTranslate-config/$SITE"
Actual:
{"message":"Cannot invoke \"com.fasterxml.jackson.databind.JsonNode.asText()\" because \"value\" is null"}
with HTTP 500.
Expected: HTTP 400 naming the offending parameter, e.g. "Secret 'apiKey' is missing a value".
Sending {"apiKey":{"hidden":true}} (a genuinely value-less secret) reproduces it just as directly.
Why callers hit this easily
SecretForm's constructor is a single-argument @JsonCreator with no @JsonProperty, which makes it a delegating creator — so the request body must be the bare map:
{"apiKey": {"value": "...", "hidden": true}}
not the arguably more natural:
{"inputParams": {"apiKey": {"value": "...", "hidden": true}}}
Anyone who guesses the second shape — reasonable, since inputParams is the field name — gets an NPE-derived 500 rather than a message telling them the shape is wrong. It took three attempts to find the accepted form.
Impact
- A malformed request produces a 500, so it reads as a server fault rather than a client error, and pollutes error dashboards.
- The response leaks an internal JVM NPE message to the API consumer. Low severity — no secret material is exposed — but poor hygiene for an endpoint whose whole job is handling credentials.
- No data is written or damaged: the failure happens during deserialization, before the secrets store is touched. Verified on a container — the store's checksum was unchanged after each failed attempt.
Acceptance Criteria
- A body where an
Inputhas novaluereturns 400, not 500, with a message naming the parameter. - The response contains no raw JVM exception text.
-
hiddenkeeps its current tolerant behaviour (absent →false). - A body wrapped in
{"inputParams": {...}}returns a 400 explaining the expected shape rather than an NPE — or is accepted, if supporting both shapes is preferred. - Unit coverage for
Input.InputDeserializeover:valuepresent,valueabsent,hiddenabsent, and an empty object. - The accepted request shape is documented — the
@Operation/@Schemaannotations onAppsResource.createAppSecretsso it lands in the generatedopenapi.yaml.
Additional Context
Pre-existing on main; not introduced by any recent change. Found while manually exercising the Apps REST API to verify the secrets-store fix in #36724 / PR #37053, which is unrelated to this defect.
Severity
Low - Cosmetic or minor issue
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Input.InputDeserialize in dotCMS/src/main/java/com/dotcms/rest/api/v1/apps/Input.java and then inspect AppsResource.createAppSecrets, including its @Operation/@Schema annotations. Add unit coverage for present and missing value, absent hidden, and an empty object; done means malformed inputs return 400 without raw JVM text, while the accepted request shape is documented in openapi.yaml.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 76/100